Напишите свой асинхронный метод
Я хотел бы знать, как написать свои собственные асинхронные методы "правильным" способом.
Я видел много сообщений, объясняющих шаблон async/await следующим образом:
http://msdn.microsoft.com/en-us/library/hh191443.aspx
// Three things to note in the signature:
// - The method has an async modifier.
// - The return type is Task or Task<T>. (See "Return Types" section.)
// Here, it is Task<int> because the return statement returns an integer.
// - The method name ends in "Async."
async Task<int> AccessTheWebAsync()
{
// You need to add a reference to System.Net.Http to declare client.
HttpClient client = new HttpClient();
// GetStringAsync returns a Task<string>. That means that when you await the
// task you'll get a string (urlContents).
Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");
// You can do work here that doesn't rely on the string from GetStringAsync.
DoIndependentWork();
// The await operator suspends AccessTheWebAsync.
// - AccessTheWebAsync can't continue until getStringTask is complete.
// - Meanwhile, control returns to the caller of AccessTheWebAsync.
// - Control resumes here when getStringTask is complete.
// - The await operator then retrieves the string result from getStringTask.
string urlContents = await getStringTask;
// The return statement specifies an integer result.
// Any methods that are awaiting AccessTheWebAsync retrieve the length value.
return urlContents.Length;
}
private void DoIndependentWork()
{
resultsTextBox.Text += "Working........\r\n";
}
Это отлично работает для любого .NET-метода, который уже реализует эту функциональность, например
- Операции System.IO
- Операции с DataBase
- Операции, связанные с сетью (загрузка, загрузка...)
Но что, если я хочу написать свой собственный метод, который занимает довольно много времени, чтобы завершить то, где просто нет метода, который я могу использовать, и большая нагрузка находится в методе DoIndependentWork
в приведенном выше примере?
В этом методе я мог бы сделать:
- Обработка строк
- Вычисления
- Обработка моих собственных объектов
- Агрегирование, сравнение, фильтрация, группировка, обработка материалов.
- Список операций, добавление, удаление, копирование
Снова я наткнулся на многие сообщения, где люди просто делают следующее (снова беря приведенный выше пример):
async Task<int> AccessTheWebAsync()
{
HttpClient client = new HttpClient();
Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");
await DoIndependentWork();
string urlContents = await getStringTask;
return urlContents.Length;
}
private Task DoIndependentWork()
{
return Task.Run(() => {
//String manipulations
//Calculations
//Handling my own objects
//Aggregating, comparing, filtering, grouping, handling stuff
//List operations, adding, removing, coping
});
}
Вы можете заметить, что изменения состоят в том, что DoIndependentWork
теперь возвращает задачу и в задаче AccessTheWebAsync
метод получил await
.
Операции с большой нагрузкой теперь закрываются внутри Task.Run()
, это все, что требуется?
Если это все, что требуется, это единственное, что мне нужно сделать, чтобы предоставить async Method для каждого отдельного метода в моей библиотеке следующее:
public class FooMagic
{
public void DoSomeMagic()
{
//Do some synchron magic...
}
public Task DoSomeMagicAsync()
{
//Do some async magic... ?!?
return Task.Run(() => { DoSomeMagic(); });
}
}
Было бы неплохо, если бы вы могли объяснить это мне, так как даже высокий голосовой вопрос вроде этого:
Как написать простой метод async? объясняет это только с уже существующими методами и просто использует шаблон asyn/await, как этот комментарий по указанному вопросу приводит его к сути:
Как написать простой метод асинхронизации?
Ответы
Ответ 1
Было бы неплохо, если бы вы могли объяснить это мне: как написать простой асинхронный метод?
Во-первых, нам нужно понять, что означает метод async
. Когда вы предоставляете метод async
конечному пользователю, потребляющему метод async
, вы говорите ему: "Слушайте, этот метод быстро вернется к вам с обещанием завершения когда-нибудь в ближайшее будущее". Это то, что вы гарантируете своим пользователям.
Теперь нам нужно понять, как Task
делает это "обещание" возможным. Как вы спрашиваете в своем вопросе, почему просто добавление Task.Run
внутри моего метода делает его действительным для ожидания с помощью ключевого слова await
?
A Task
реализует шаблон GetAwaiter
, то есть возвращает объект с именем awaiter
(его фактически называют TaskAwaiter
). Объект TaskAwaiter
реализует либо INotifyCompletion
, либо ICriticalNotifyCompletion
, выставляя метод OnCompleted
.
Все эти плюсы, в свою очередь, используются компилятором, когда используется ключевое слово await
. Компилятор будет следить за тем, чтобы во время разработки ваш объект реализовал GetAwaiter
и, в свою очередь, использовал его для компиляции кода в конечный автомат, что позволит вашей программе вернуть управление вызывающему абоненту после ожидания и возобновить, когда это работа завершена.
Теперь есть некоторые рекомендации. Настоящий асинхронный метод не использует лишние потоки за сценой, чтобы выполнять свою работу (Стефан Клири прекрасно объясняет это в Нет нити), что означает что разоблачение метода, который использует Task.Run
внутри, немного вводит в заблуждение потребителей вашего api, потому что они не будут принимать никаких дополнительных потоков, связанных с вашей задачей. Что вам нужно сделать, это разоблачить ваш API синхронно и позволить пользователю выгрузить его с помощью Task.Run
самостоятельно, контролируя поток выполнения.
Методы async
в основном используются для операций I/O Bound, поскольку они, естественно, не нуждаются ни в каких потоках, которые будут использоваться во время выполнения операции ввода-вывода, и поэтому мы видим их много в классах, ответственных за выполнение операций ввода-вывода, таких как вызовы жесткого диска, сетевые вызовы и т.д.
Я предлагаю прочитать статью о командах Parallel PFX Должен ли я открывать асинхронные обертки для синхронных методов?, в которых говорится о том, что вы пытаетесь сделать и почему это не рекомендуется.
Ответ 2
Фактический ответ
Вы делаете это с помощью TaskCompletionSource
, в котором есть Задача обещания, которая не выполняет никаких код и только:
"Представляет сторону производителя задачи, не связанной с делегатом, обеспечивая доступ к стороне потребителя через свойство Task."
Вы возвращаете эту задачу вызывающему абоненту при запуске асинхронной операции, и вы устанавливаете результат (или исключение/отмену), когда вы его завершаете. Убедитесь, что операция действительно асинхронна.
Вот хороший пример этого корня всех асинхронных методов в Stephen Toub AsyncManualResetEvent
:
class AsyncManualResetEvent
{
private volatile TaskCompletionSource<bool> _tcs = new TaskCompletionSource<bool>();
public Task WaitAsync() { return _tcs.Task; }
public void Set() { _tcs.TrySetResult(true); }
public void Reset()
{
while (true)
{
var tcs = _tcs;
if (!tcs.Task.IsCompleted ||
Interlocked.CompareExchange(ref _tcs, new TaskCompletionSource<bool>(), tcs) == tcs)
return;
}
}
}
Фон
В принципе есть две причины использовать async-await
:
- Улучшенная масштабируемость. При интенсивной работе
I/O
(или других асинхронных операциях) вы можете вызывать ее асинхронно, и поэтому вы освобождаете вызывающий поток и можете выполнять другую работу в среднее время.
- Разгрузка:. Когда у вас интенсивная работа
CPU
, вы можете вызвать ее асинхронно, что перемещает работу одного потока на другой (в основном используется для потоков GUI
).
Таким образом, большинство асинхронных вызовов. Net
поддерживают async
из коробки, а для разгрузки вы используете Task.Run
(как в вашем примере). Единственный случай, когда вам действительно нужен реализовать async
самостоятельно, - это когда вы создаете новый асинхронный вызов (I/O
или async конструкции синхронизации например).
Эти случаи крайне редки, поэтому вы чаще всего находите ответы, которые
"Только объясняет это уже существующими методами и просто использует шаблон async/await
Вы можете пойти глубже в The Nature of TaskCompletionSource
Ответ 3
TL; DR:
Task.Run()
- это то, что вы хотите, но будьте осторожны, скрывая его в своей библиотеке.
Я мог ошибаться, но вы могли бы найти руководство по получить код с привязкой к CPU, который будет выполняться асинхронно [by Stephen Cleary]. Мне тоже было сложно найти это, и я думаю, что причина, по которой это так сложно, - это не то, что вы должны делать для библиотеки - своего рода...
Статья
Связанная статья - хорошее чтение (5-15 минут, в зависимости), которое идет в приличное количество подробностей о hows и whys использования Task.Run()
как части API против использования его, чтобы не блокировать поток пользовательского интерфейса - и различает два "типа" долговременного процесса, которые люди любят запускать асинхронно:
- Процесс с привязкой к процессору (тот, который хруст/фактически работает и нуждается в отдельном потоке для выполнения своей работы)
- Поистине асинхронная операция (иногда называемая IO-bound - одна, которая делает несколько вещей здесь и там с кучей времени ожидания между действиями, и было бы лучше не забивать нить, пока она сидя там ничего не делая).
В статье затрагивается использование функций API в различных контекстах и объясняется, соответствуют ли ассоциированные архитектуры "синхронным" или "асинхронным" методам, а также как "API" с сигнатурами sync и async "смотрит" на разработчика.
Ответ
Последний раздел " ОК, достаточно о неправильных решениях, как мы это исправим правильно?" идет в то, о чем я думаю, о чем вы спрашиваете, заканчивая этим:
Заключение: не используйте Task.Run в реализации метода; вместо этого используйте Task.Run для вызова метода.
В принципе, Task.Run()
"hogs" поток, и, следовательно, это то, что нужно использовать для работы с ЦП, но оно сводится к тому, где оно используется. Когда вы пытаетесь выполнить что-то, требующее большой работы, и вы не хотите блокировать поток пользовательского интерфейса, используйте Task.Run()
для непосредственного запуска рабочей функции (то есть в обработчике событий или в вашем пользовательском интерфейсе код):
class MyService
{
public int CalculateMandelbrot()
{
// Tons of work to do in here!
for (int i = 0; i != 10000000; ++i)
;
return 42;
}
}
...
private async void MyButton_Click(object sender, EventArgs e)
{
await Task.Run(() => myService.CalculateMandelbrot());
}
Но... не скрывайте свой Task.Run()
в функции API суффикс -Async
, если это функция, привязанная к процессору, так как в основном каждая функция -Async
действительно асинхронна, а не связана с ЦП.
// Warning: bad code!
class MyService
{
public int CalculateMandelbrot()
{
// Tons of work to do in here!
for (int i = 0; i != 10000000; ++i)
;
return 42;
}
public Task<int> CalculateMandelbrotAsync()
{
return Task.Run(() => CalculateMandelbrot());
}
}
Другими словами, не вызывайте функцию, привязанную к процессору -Async
, потому что пользователи предполагают, что она привязана к IO - просто вызывайте ее асинхронно с помощью Task.Run()
, и пусть другие пользователи делают то же самое, когда они это чувствуют подходящее. В качестве альтернативы, назовите это что-то другое, что имеет смысл для вас (возможно, BeginAsyncIndependentWork()
или StartIndependentWorkTask()
).