Подтвердить что ты не робот

Напишите свой асинхронный метод

Я хотел бы знать, как написать свои собственные асинхронные методы "правильным" способом.

Я видел много сообщений, объясняющих шаблон 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, как этот комментарий по указанному вопросу приводит его к сути: Как написать простой метод асинхронизации?

4b9b3361

Ответ 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()).