Опубликован: 28.06.2006 | Уровень: специалист | Доступ: платный | ВУЗ: Московский государственный технический университет им. Н.Э. Баумана
Лекция 15:

Параллельные операции в .NET

Асинхронные процедуры

Для реализации вызова асинхронных процедур в .NET используются фоновые потоки пула, так же как для обработки асинхронных операций ввода-вывода. Класс ThreadPool предлагает два способа для вызова асинхронных процедур: явное размещение вызовов в очереди ( QueueUserWorkItem ) и связывание вызовов с переводом некоторых объектов в свободное состояние ( RegisterWaitForSingleObject ). Кроме того, .NET позволяет осуществлять асинхронные вызовы любых процедур с помощью метода BeginInvoke делегатов.

Статический метод ThreadPool.QueueUserWorkItem ставит вызов указанной процедуры в очередь для обработки. Если пул содержит простаивающие потоки, то обработка этой функции начнется немедленно:

using System;
using System.Threading;
namespace TestNamespace {
  class GreetingData {
    private string   m_greeting;
    public GreetingData( string text ) { m_greeting = text; }
    public void Invoke() { Console.WriteLine( m_greeting ); }
  }

  class TestApp {
    static void AsyncProc( Object arg )
    {
      GreetingData  gd = (GreetingData)arg;
      gd.Invoke();
    }

    public static void Main()
    {
      GreetingData gd = new GreetingData("Hello, world!");
      ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncProc), gd);
      Thread.Sleep( 1000 );
    }
  }
}

При постановке в очередь асинхронного вызова можно указать объект, который является аргументом асинхронной процедуры (при создании собственных потоков передача аргументов процедуре потока затруднительна).

Второй способ вызова асинхронных процедур связан с использованием объектов, производных от класса System.Threading.WaitHandle (это события и мьютексы). При этом вызов асинхронной процедуры связывается с переводом объекта в свободное состояние. Данный метод может быть использован также для организации повторяющегося через определенные интервалы вызова асинхронных процедур - при регистрации делегата указывается максимальный интервал ожидания, и если он исчерпывается, то вызов размещается в очереди пула, даже если объект остался занятым. Если объект по-прежнему остается занятым, то вызов процедуры будет периодически размещаться в очереди после исчерпания каждого интервала ожидания.

using System;
using System.Threading;

namespace TestNamespace {
  class GreetingData {
    private string                m_greeting;
    private RegisteredWaitHandle   m_waithandle;
    public GreetingData( string text ) { m_greeting = text; }
    public void Invoke() { Console.WriteLine( m_greeting ); }
    public RegisteredWaitHandle WaitHandle {
      set {
        if (value==null) m_waithandle.Unregister( null );
        m_waithandle = value;
      }
    }
  }
  class TestApp {
    static void AsyncProc( Object arg, bool isTimeout ) {
      GreetingData  gd = (GreetingData)arg;
      if ( !isTimeout ) gd.WaitHandle = null;
      gd.Invoke();
    }
    public static void Main() {
      GreetingData     gd = new GreetingData("Hello");
      AutoResetEvent   ev = new AutoResetEvent(false);
      gd.WaitHandle=ThreadPool.RegisterWaitForSingleObject(
        ev,  new WaitOrTimerCallback(AsyncProc),
        gd,  1000,  false
      );
      Thread.Sleep( 2500 );
      ev.Set();
      Console.ReadLine();
    }
  }
}

Приведенный пример демонстрирует использование периодического вызова асинхронной процедуры - при регистрации делегата ( RegisterWaitForSingleObject ) указывается максимальное время ожидания 1 секунда (1000 миллисекунд), после чего основной поток переводится в состояние "спячки" на 2.5 секунды. За это время в очередь пула поступает два вызова асинхронных процедур (с признаком вызова по тайм-ауту). Через 2.5 секунды основной поток пробуждается, переводит событие в свободное состояние, и в очередь пула поступает третий вызов. При обработке этого вызова регистрация делегата отменяется.

Последний способ связан с использованием методов BeginInvoke и EndInvoke делегатов. Когда определяется какой-либо делегат функции, для него будут определены методы: BeginInvoke (содержащий все аргументы делегата плюс два дополнительных - AsyncCallback, который может быть вызван по завершении обработки асинхронного вызова, и AsyncState, с помощью которого можно определить состояние асинхронной процедуры) и EndInvoke, содержащий все выходные параметры (т.е. описанные как inout или out ), плюс IAsyncResult, позволяющий узнать результат выполнения процедуры.

Таким образом, использование BeginInvoke позволяет не только поставить в очередь вызов асинхронной процедуры, но также связать с завершением ее обработки еще один асинхронный вызов. Метод EndInvoke служит для ожидания завершения обработки асинхронной процедуры:

using System;
using System.Threading;

namespace TestNamespace {
  public class GreetingData {
    private string   m_greeting;
    public GreetingData( string text ) { m_greeting = text; }
    public static void Invoke( GreetingData arg ) {
      Console.WriteLine( arg.m_greeting );
    }
  }
  public delegate void AsyncProcCallback ( GreetingData gd );
  class TestApp {
    public static void Main() {
      GreetingData gd = new GreetingData( "Hello!!!" );
      AsyncProcCallback apd = new AsyncProcCallback(
        GreetingData.Invoke );
      IAsyncResult ar = apd.BeginInvoke( gd, null, null );
      ar.AsyncWaitHandle.WaitOne();
    }
  }
}

Данный пример иллюстрирует вызов асинхронной процедуры с использованием метода BeginInvoke и альтернативный механизм ожидания завершения - с использованием внутреннего объекта AsyncWaitHandle (класса WaitHandle ), благодаря которому, собственно говоря, становится возможен вызов асинхронной процедуры, обслуживающей завершение обработки данной процедуры.

В этом смысле асинхронный вызов процедур с помощью BeginInvoke очень близок к обработке асинхронных операций ввода-вывода.

Анастасия Булинкова
Анастасия Булинкова
Рабочим названием платформы .NET было
Bogdan Drumov
Bogdan Drumov
Молдова, Республика
Azamat Nurmanbetov
Azamat Nurmanbetov
Киргизия, Bishkek