Как вы создаете асинхронный метод в 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

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


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;
}

В приложении WinForms остальная часть метода завершается в том же потоке (поток пользовательского интерфейса). Это Task.Yield()действительно полезно для случаев, когда вы хотите убедиться, что возвращенное Taskне будет немедленно завершено при создании.
Теодор Зулиас
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.