24 мая 2014

Паттерны асинхронного программирования в .NET

Паттерны асинхронного программирования в .NET

Большинство современных приложений устроены так, что им необходимо постоянное взаимодействие с миром: получение данных из БД, отправка запросов на внешний web-ресурс, ожидание ввода пользователя.

Наиболее привычный синхронный вызов таких взаимодействий приводит к простаиванию потоков в ожидании ответов, к избыточному расходованию оперативной памяти (потоки впустую занимают память). Все это является причиной снижения производительности приложения, а также его невысокой способности к масштабированию.

Запросы к веб-сервисам и к внешним ресурсам (такие как, базы данных), запросы, интенсивно использующие I/O-операции - хорошей практикой в описанных случаях является использование шаблонов асинхронного программирования - способа выполнения длительных операций без блокировки вызывающего потока.

Выделают следующие паттерны асинхронного программирования:

  • асинхронный шаблон или Asynchronous Programming Model (APM);
  • асинхронный шаблон, основанный на событиях, или Event-based Asynchronous Pattern (EAP);
  • асинхронный шаблон, основанный на задачах, или Task-based Asynchronous Pattern (TAP).

В .NET модель AРМ появилась еще в первой версии фреймворка .NET. В .NET Framework 2.0 появилась модель EAP. TAP-паттерн базируется на типе Task, появившемся в .NET 4.0, и применении ключевых слов async и await, появившихся в компиляторе C# версии 5.

В API следующих классов есть поддержка вызовов асинхронных методов (доступно в .NET Framework 4.5):

  • работа c web-ресурсами: System.Net.Http.HttpClient, System.Net.WebRequest, System.Net.Sockets.Socket, System.Net.Dns, etc.;
  • работа с web-сервисами: инструменты генерации прокси для веб-сервисов (wsdl.exe и svcutil.exe) генерируют код вызова методов служб в соответствии с паттернами APM, EAP, TAP;
  • работа с файловой системой: StorageFile, StreamWriter, StreamReader, XmlReader;
  • работа с базами данных: System.Data.SqlClient.SqlCommand;
  • работа с графикой: MediaCapture, BitmapEncoder, BitmapDecoder.

Ниже обзорно рассмотрен каждый из паттернов асинхронного программирования, а также приведены примеры вызовов WCF-служб с использованием каждого из перечисленных шаблонов.

Введем некоторый класс, имеющий метод, описанных следующим псевдокодом:

public TResult {MethodName}(TIn[] args) { … }

И сервис MessageService:

public string SendMessage(string userName, string message)
{
Contract.Requires<ArgumentNullException>(userName != null);
Contract.Requires<ArgumentNullException>(message != null);

Thread.Sleep(2000); // немного сна на рабочем месте)

return String.Format("{0} send '{1}'", userName, message);
}

Синхронный вызов MessageService выглядит так:

using (var client = new MessageServiceClient())
{
try
{
var actual = client.SendMessage("@codezombi", "Send sync");
client.Close();

Console.WriteLine(actual);
}
finally
{
if (client.State != CommunicationState.Closed)
client.Abort();
}
}

И возвращает следующий вывод:

Start main()
@codezombi send 'Send sync'
Time elapsed: 00:00:02.3077486
End main()

Asynchronous Programming Model (APM)

Asynchronous Programming Model (APM) - модель асинхронного программирования, основанная на следующем шаблоне:

public IAsyncResult Begin{MethodName}(TIn[] args, AsyncCallback callback, object userState = null) { … }
public TResult End{MethodName}(IAsyncResult result) { ... }

где
Begin{MethodName}: начинает асинхронное выполнение операции {MethodName},
принимает параметры args, совпадающие с сигнатурой синхронного метода; делегат System.AsyncCalback, указывающий метод, вызываемый при завершении асинхронной операции; и объект состояния userState, содержащий дополнительные сведения об асинхронной операции,
и возвращает объект, реализующий интерфейс System.IAsyncResult.
End{MethodName}, соответственно:
заканчивает асинхронное выполнение операции;
принимает объект, реализующий IAsyncResult, и возвращает с типом TResult, совпадающим с синхронной версией метода {MethodName}.

Вызов веб-службы для паттерна APM будет выглядеть так:

static void SendMessageAsyncAPM()
{
var client = new MessageServiceClient();

client.BeginSendMessage("@codezombi", "Send async [APM]",
ar =>
{
var c = (MessageServiceClient)ar.AsyncState;

var actual = c.EndSendMessage(ar);

Console.WriteLine(actual);
},
client);
}

Имеем следующий вывод:

Start main()
Invoked: 00:00:00.2706677
End main()
Callback: 00:00:02.3433612
@codezombi send 'Send async [APM]'

У модели APM есть следующие недостатки:

  • необходимость написания кода обратного вызова для всех асинхронных операций;
  • невозможность использования «привычного» синтаксиса, такого как: использование конструкции using… finally для открытия и закрытия канала;
  • сложность реализации отмены операции, реакции на таймаут ответа сервиса.

Event-based Asynchronous Pattern (EAP)

Event-based Asynchronous Pattern (EAP) - асинхронный шаблон, основанный на событиях и описываемый в простейшем случае следующим псевдокодом:

public void {MethodName}Async(TInput[] args, object userState = null)
{
InvokeAsync(onBegin{MethodName}Delegate, args, onEnd{MethodName}Delegate, on{MethodName}CompletedDelegate, userState);
}

EventHandler<{MethodName}CompletedEventArgs> {MethodName}Completed;

private void On{MethodName}Completed(object state) { … }

/// <param name="beginOperationDelegate">Делегат, используемые для вызова асинхронной операции</param>
/// <param name="inValues">Входные значения вызова</param>
/// <param name="endOperationDelegate">Делегат, используемые для окончания асинхронного вызова</param>
/// <param name="operationCompletedCallback">Обратный вызов (также делегат), вызываемый, когда асинхронный метод выполнится. Передается в <see cref="T:System.ServiceModel.ClientBase`1.BeginOperationDelegate"/>.</param>
/// <param name="userState">Объект состояния, ассоциированный с асинхронным вызовом</param>
void InvokeAsync(BeginOperationDelegate beginOperationDelegate, object[] inValues, EndOperationDelegate endOperationDelegate, SendOrPostCallback operationCompletedCallback, object userState);

где
{MethodName}Async() начинает асинхронное выполнение операции {MethodName}; принимает параметры args, совпадающие с сигнатурой синхронного метода, и необязательный параметр userState, содержащий дополнительные сведения об асинхронной операции.
{MethodName}Completed - событие, наступающее, когда асинхронный метод выполнился.

Вызов веб-службы для паттерна EAP:

static void SendMessageAsyncEAP()
{
var client = new MessageServiceClient();

client.SendMessageCompleted += (sender, args) =>
{
var actual = args.Result;

Console.WriteLine(actual);
};

client.SendMessageAsync("@codezombi", "Send async [EAP]");
}

Вывод:

Start main()
Invoked: 00:00:00.5665485
End main()
Callback: 00:00:02.6351425
@codezombi send 'Send async [EAP]'

Использовать EAP или APM при написании асинхронного кода – в целом, дела вкуса. EAP не решает ни одной из проблем свойственных шаблону APM. А именно:

  • для вызова для всех асинхронных операций все также необходимо писать дополнительный код (хотя уже и нет необходимости в введении дополнительных аргументов asyncCallaback и userState);
  • невозможно использовать конструкции using… finally для открытия и закрытия канала;
  • сложность реализации отмены операции, перехвата таймаута ответа сервиса.

Task-based Asynchronous Pattern (TAP)

Task-based Asynchronous Pattern (TAP) - асинхронный шаблон, основанный на задачах. В .NET шаблон TAP базируется на классах Task и Task<T>, а также на ключевых словах C#: async и await.

Паттерн TAP является рекомендуемым шаблоном разработки в .NET [1].

Паттерн TAP является более изящным по сравнению с APM и EAP и не требует написания/генерации дополнительных методов с префиксами Begin/End или постфиксом Async.

Вызов веб-службы для паттерна TAP:

static async Task<string> SendMessageAsync()
{
var client = new MessageServiceClient();

var task = Task.Factory.StartNew(() => client.SendMessage("@codezombi", "Send async [TAP]"));
var actual = await task;

return actual;
}

Вывод:

Start main()
Elapsed time: 00:00:02.2998571
@codezombi send 'Send async [TAP]'
End main()

TAP. Yet another Implementation

И последний пример вызова сервиса, который будет показан, основан на «task-based»-режиме генерации WCF-прокси.

Этот вызов является частным случаем уже обсужденного паттерна TAP, и имеет, пожалуй, самый изящный код вызова.

Вызов «task-based»-операции веб-службы:

static async Task<string> SendMessageAsyncNew()
{
var client = new MessageTaskServiceClient();
var actual = await client.SendMessageAsync("@codezombi", "Send async [TAP]");

return actual;
}

Вывод:

Start main()
End main()
Elapsed time: 00:00:02.2863746
@codezombi send 'Send async [TAP]'

Дополнительные источники

Автор статьи

,
Machine Learning Preacher, Microsoft AI MVP && Coffee Addicted