Як створити асинхронний метод у 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 - це те, що я шукав.
Рицибоб

1
Стівен, в циклі for, наступна ітерація викликається негайно або після Task.Delayповернення?
jp2code

3
@ jp2code: await"асинхронне очікування", тому він не переходить до наступної ітерації, поки не Task.Delayзавершиться завдання, яке повертається .
Стівен Клірі

12

Якщо ви не хочете використовувати async / чакати всередині свого методу, але все-таки "прикрасьте" його так, щоб мати можливість використовувати ключове слово очікування ззовні, 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 та робить деякі оптимізації з можливістю планування та запуску.
небезпечноPtr

тож, що ви зробили з ThreadPool.QueueUserWorkItem можна було зробити з Task.Run, правда?
Разван

-1

Один дуже простий спосіб зробити метод асинхронним - це використовувати метод Task.Yield (). Як зазначає MSDN:

Ви можете використовувати функцію очікування 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 решта методу буде виконана в одному потоці (потік UI). Це Task.Yield()дійсно корисно для тих випадків, коли ви хочете переконатися, що повернене Taskне буде негайно завершено після створення.
Теодор Зуліяс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.