Как вы создаете асинхронный метод в C #?

196

Каждый пост в блоге, который я читал, рассказывает вам, как использовать асинхронный метод в C #, но по какой-то странной причине никогда не объясняйте, как создавать свои собственные асинхронные методы для потребления. Итак, у меня есть этот код прямо сейчас, который использует мой метод:

private async void button1_Click(object sender, EventArgs e)
{
    var now = await CountToAsync(1000);
    label1.Text = now.ToString();
}

И я написал этот метод, который CountToAsync:

private Task<DateTime> CountToAsync(int num = 1000)
{
    return Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < num; i++)
        {
            Console.WriteLine("#{0}", i);
        }
    }).ContinueWith(x => DateTime.Now);
}

Является ли это использование Task.Factoryлучшего способа написания асинхронного метода, или я должен написать это по-другому?

Халид Абухакмех
источник
22
Я задаю общий вопрос о том, как структурировать метод. Я просто хочу знать, с чего начать превращение моих уже синхронных методов в асинхронные.
Халид Абухакмех
2
Итак, что же типичный синхронный метод делать , и почему вы хотите сделать это асинхронно ?
Эрик Липперт
Допустим, мне нужно выполнить пакетную обработку множества файлов и вернуть объект результата.
Халид Абухакмех
1
Хорошо, так: (1) что такое операция с высокой задержкой: получение файлов - потому что сеть может быть медленной или что-то в этом роде - или выполнение обработки - потому что, скажем, она интенсивно использует процессор. И (2) вы все еще не сказали, почему вы хотите, чтобы он был асинхронным в первую очередь. Есть ли поток пользовательского интерфейса, который вы не хотите блокировать, или как?
Эрик Липперт
21
@EricLippert Пример, приведенный операцией, очень прост, он не должен быть таким сложным.
Дэвид Б.

Ответы:

227

Я не рекомендую, StartNewесли вам не нужен этот уровень сложности.

Если ваш асинхронный метод зависит от других асинхронных методов, самый простой подход - использовать asyncключевое слово:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  for (int i = 0; i < num; i++)
  {
    await Task.Delay(TimeSpan.FromSeconds(1));
  }

  return DateTime.Now;
}

Если ваш асинхронный метод выполняет работу с процессором, вы должны использовать Task.Run:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  await Task.Run(() => ...);
  return DateTime.Now;
}

Вы можете найти мое async/ awaitвступление полезным.

Стивен Клири
источник
10
@Stephen: «Если ваш асинхронный метод зависит от других асинхронных методов» - хорошо, но что, если это не так. Что если бы мы пытались обернуть некоторый код, который вызывает BeginInvoke, в некоторый код обратного вызова?
Ricibob
1
@Ricibob: Вы должны использовать, TaskFactory.FromAsyncчтобы обернуть BeginInvoke. Не уверен, что вы имеете в виду под "кодом обратного вызова"; не стесняйтесь оставить свой вопрос с кодом.
Стивен Клири
@Stephen: Спасибо - да TaskFactory.FromAsync - это то, что я искал.
Ricibob
1
Стивен, в цикле for вызывается следующая итерация сразу или после Task.Delayвозврата?
jp2code
3
@ jp2code: awaitэто «асинхронное ожидание», поэтому он не переходит к следующей итерации, пока не Task.Delayзавершится задание, возвращаемое .
Стивен Клири
12

Если вы не хотите использовать async / await внутри вашего метода, но по-прежнему «украшаете» его, чтобы иметь возможность использовать ключевое слово await извне, TaskCompletionSource.cs :

public static Task<T> RunAsync<T>(Func<T> function)
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ =>          
    { 
        try 
        {  
           T result = function(); 
           tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
   }); 
   return tcs.Task; 
}

Отсюда и здесь

Чтобы поддержать такую ​​парадигму с Задачами, нам нужен способ сохранить фасад Задачи и возможность ссылаться на произвольную асинхронную операцию как Задачу, но управлять временем жизни этой Задачи в соответствии с правилами базовой инфраструктуры, обеспечивающей асинхронность, и делать это так, чтобы это не стоило значительно. Это цель TaskCompletionSource.

Я видел также используется в источнике .NET, например. WebClient.cs :

    [HostProtection(ExternalThreading = true)]
    [ComVisible(false)]
    public Task<string> UploadStringTaskAsync(Uri address, string method, string data)
    {
        // Create the task to be returned
        var tcs = new TaskCompletionSource<string>(address);

        // Setup the callback event handler
        UploadStringCompletedEventHandler handler = null;
        handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadStringCompleted -= completion);
        this.UploadStringCompleted += handler;

        // Start the async operation.
        try { this.UploadStringAsync(address, method, data, tcs); }
        catch
        {
            this.UploadStringCompleted -= handler;
            throw;
        }

        // Return the task that represents the async operation
        return tcs.Task;
    }

Наконец, я нашел полезным также следующее:

Мне постоянно задают этот вопрос. Подразумевается, что где-то должен быть какой-то поток, который блокирует вызов ввода-вывода для внешнего ресурса. Итак, асинхронный код освобождает поток запросов, но только за счет другого потока в другом месте системы, верно? Нет, совсем нет. Чтобы понять, почему асинхронные запросы масштабируются, я прослежу (упрощенный) пример асинхронного вызова ввода-вывода. Допустим, запрос должен быть записан в файл. Поток запроса вызывает метод асинхронной записи. WriteAsync реализуется библиотекой базовых классов (BCL) и использует порты завершения для своего асинхронного ввода-вывода. Итак, вызов WriteAsync передается в ОС в виде асинхронной записи в файл. Затем ОС связывается со стеком драйверов, передавая данные для записи в пакет запроса ввода-вывода (IRP). Вот где все становится интересным: Если драйвер устройства не может обработать IRP немедленно, он должен обработать его асинхронно. Таким образом, драйвер сообщает диску начать запись и возвращает «ожидающий» ответ ОС. ОС передает этот «ожидающий» ответ в BCL, а BCL возвращает незавершенную задачу в код обработки запросов. Код обработки запросов ожидает задачу, которая возвращает неполную задачу из этого метода и так далее. Наконец, код обработки запроса в итоге возвращает неполную задачу в ASP.NET, и поток запроса освобождается для возврата в пул потоков. Код обработки запросов ожидает задачу, которая возвращает неполную задачу из этого метода и так далее. Наконец, код обработки запроса в итоге возвращает неполную задачу в ASP.NET, и поток запроса освобождается для возврата в пул потоков. Код обработки запросов ожидает задачу, которая возвращает неполную задачу из этого метода и так далее. Наконец, код обработки запроса в итоге возвращает неполную задачу в ASP.NET, и поток запроса освобождается для возврата в пул потоков.

Введение в Async / Await на ASP.NET

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

Alberto
источник
1
Если вы увидите комментарий над реализацией Task.Run, например, ставит в очередь указанную работу для выполнения на ThreadPool и возвращает дескриптор задачи для этой работы . Вы на самом деле делаете то же самое. Я уверен, что Task.Run будет лучше, потому что он внутренне управляет CancelationToken и делает некоторые оптимизации с расписанием и опциями запуска.
unsafePtr
так что то, что вы сделали с ThreadPool.QueueUserWorkItem, может быть сделано с Task.Run, верно?
Разван
-1

Один очень простой способ сделать метод асинхронным - это использовать метод Task.Yield (). Как говорится в MSDN:

Вы можете использовать await Task.Yield (); в асинхронном методе, чтобы заставить метод завершиться асинхронно.

Вставьте его в начало вашего метода, и он немедленно вернется к вызывающей стороне и завершит оставшуюся часть метода в другом потоке.

private async Task<DateTime> CountToAsync(int num = 1000)
{
    await Task.Yield();
    for (int i = 0; i < num; i++)
    {
        Console.WriteLine("#{0}", i);
    }
    return DateTime.Now;
}
SalgoMato
источник
В приложении WinForms остальная часть метода завершается в том же потоке (поток пользовательского интерфейса). Это Task.Yield()действительно полезно для случаев, когда вы хотите убедиться, что возвращенное Taskне будет немедленно завершено при создании.
Теодор Зулиас