Jak utworzyć metodę asynchroniczną w języku C #?

196

Każdy post na blogu, który przeczytałem, mówi ci, jak korzystać z metody asynchronicznej w języku C #, ale z jakiegoś dziwnego powodu nigdy nie wyjaśniaj, jak zbudować własne metody asynchroniczne. Mam teraz ten kod, który zużywa moją metodę:

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

I napisałem tę metodę, która jest 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);
}

Czy to jest użycie Task.Factorynajlepszego sposobu na napisanie metody asynchronicznej, czy powinienem napisać to w inny sposób?

Khalid Abuhakmeh
źródło
22
Zadaję ogólne pytanie na temat struktury metody. Chcę tylko wiedzieć, od czego zacząć przekształcanie moich już synchronicznych metod w asynchroniczne.
Khalid Abuhakmeh
2
OK, więc co to typowy sposób synchroniczny zrobić i dlaczego chcesz zrobić to asynchroniczny ?
Eric Lippert
Powiedzmy, że muszę wsadowo przetworzyć kilka plików i zwrócić obiekt wynikowy.
Khalid Abuhakmeh
1
OK, więc: (1) co to jest operacja o dużym opóźnieniu: uzyskanie plików - ponieważ sieć może być wolna lub cokolwiek - lub wykonywanie przetwarzania - ponieważ, powiedzmy, jest to procesor intensywny. I (2) nadal nie powiedziałeś, dlaczego chcesz, aby był on asynchroniczny. Czy istnieje wątek interfejsu użytkownika, którego nie chcesz blokować?
Eric Lippert 17.04.13
21
@EricLippert Przykład podany przez op jest bardzo prosty, naprawdę nie musi być tak skomplikowany.
David B.

Odpowiedzi:

226

Nie polecam, StartNewchyba że potrzebujesz takiego poziomu złożoności.

Jeśli metoda asynchroniczna jest zależna od innych metod asynchronicznych, najłatwiejszym sposobem jest użycie asyncsłowa kluczowego:

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

Jeśli Twoja metoda asynchroniczna wykonuje pracę procesora, powinieneś użyć Task.Run:

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

Pomocne może okazać się moje async/ awaitwprowadzenie .

Stephen Cleary
źródło
10
@Stephen: „Jeśli twoja metoda asynchroniczna jest zależna od innych metod asynchronicznych” - ok, ale co, jeśli tak nie jest. Co się stanie, jeśli spróbuję zawinąć kod wywołujący BeginInvoke z kodem wywołania zwrotnego?
Ricibob
1
@Ricibob: Powinieneś użyć TaskFactory.FromAsyncdo zawijania BeginInvoke. Nie jestem pewien, co masz na myśli przez „kod zwrotny”; możesz napisać własne pytanie z kodem.
Stephen Cleary
@Stephen: Dzięki - tak TaskFactory.FromAsync jest tym, czego szukałem.
Ricibob
1
Stephen, w pętli for, czy następna iteracja jest wywoływana natychmiast czy po Task.Delaypowrocie?
jp2code
3
@ jp2code: awaitto „asynchroniczne oczekiwanie”, więc nie przechodzi do następnej iteracji, dopóki zadanie nie zostanie Task.Delayzakończone przez .
Stephen Cleary,
11

Jeśli nie chcesz używać async / await w swojej metodzie, ale nadal „dekorujesz” ją, aby móc użyć słowa kluczowego „czekaj” z zewnątrz, 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; 
}

Stąd i tutaj

Aby wesprzeć taki paradygmat w zadaniach, potrzebujemy sposobu na zachowanie fasady zadania i możliwość odwołania się do arbitralnej operacji asynchronicznej jako zadania, ale do kontrolowania czasu życia tego zadania zgodnie z regułami podstawowej infrastruktury, która zapewnia asynchronię, i robienie tego w sposób, który nie kosztuje znacząco. To jest cel TaskCompletionSource.

Widziałem, że jest również używany w źródle .NET np. 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;
    }

Wreszcie znalazłem przydatne również następujące:

Cały czas zadaje mi to pytanie. Sugeruje to, że musi istnieć jakiś wątek, który blokuje wywołanie We / Wy do zasobu zewnętrznego. A zatem kod asynchroniczny uwalnia wątek żądania, ale tylko kosztem innego wątku w innym miejscu w systemie, prawda? Nie, wcale nie. Aby zrozumieć, dlaczego skalowane są żądania asynchroniczne, prześledzę (uproszczony) przykład asynchronicznego wywołania We / Wy. Powiedzmy, że żądanie musi zostać zapisane do pliku. Wątek żądania wywołuje metodę zapisu asynchronicznego. WriteAsync jest implementowany przez bibliotekę Base Class Library (BCL) i używa portów zakończenia dla swoich asynchronicznych operacji we / wy. Tak więc wywołanie WriteAsync jest przekazywane do systemu operacyjnego jako asynchroniczny zapis pliku. System operacyjny komunikuje się następnie ze stosem sterowników, przekazując dane w celu zapisania w pakiecie żądania we / wy (IRP). Tutaj rzeczy stają się interesujące: Jeśli sterownik urządzenia nie może natychmiast obsłużyć IRP, musi obsłużyć go asynchronicznie. Tak więc sterownik każe dyskowi rozpocząć zapisywanie i zwraca odpowiedź „oczekującą” do systemu operacyjnego. System operacyjny przekazuje tę „oczekującą” odpowiedź do BCL, a BCL zwraca niekompletne zadanie do kodu obsługi żądań. Kod obsługi żądań oczekuje na zadanie, które zwraca niepełne zadanie z tej metody i tak dalej. Na koniec kod obsługi żądań kończy zwracanie niekompletnego zadania do ASP.NET, a wątek żądania może powrócić do puli wątków. Kod obsługi żądań oczekuje na zadanie, które zwraca niepełne zadanie z tej metody i tak dalej. Na koniec kod obsługi żądań kończy zwracanie niekompletnego zadania do ASP.NET, a wątek żądania może powrócić do puli wątków. Kod obsługi żądań oczekuje na zadanie, które zwraca niepełne zadanie z tej metody i tak dalej. Na koniec kod obsługi żądań kończy zwracanie niekompletnego zadania do ASP.NET, a wątek żądania może powrócić do puli wątków.

Wprowadzenie do Async / Await na ASP.NET

Jeśli celem jest poprawa skalowalności (a nie responsywności), wszystko zależy od istnienia zewnętrznego wejścia / wyjścia, który daje taką możliwość.

Alberto
źródło
1
Jeśli zobaczysz komentarz nad implementacją Task.Run, np. Ustawia w kolejce określoną pracę do uruchomienia w ThreadPool i zwraca uchwyt zadania dla tej pracy . W rzeczywistości robisz to samo. Jestem pewien, że Task.Run będzie lepszy, ponieważ wewnętrznie zarządza CancelationToken i wprowadza pewne optymalizacje z opcjami planowania i uruchamiania.
niebezpiecznePtr
więc to, co zrobiłeś z ThreadPool.QueueUserWorkItem, można zrobić za pomocą Task.Run, prawda?
Razvan
-1

Jednym bardzo prostym sposobem na uczynienie metody asynchroniczną jest użycie metody Task.Yield (). Jak stwierdza MSDN:

Możesz użyć funkcji wyczekiwania Task.Yield (); w metodzie asynchronicznej, aby wymusić asynchroniczne zakończenie metody.

Wstaw go na początku metody, a następnie natychmiast powróci do programu wywołującego i dokończy resztę metody w innym wątku.

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
źródło
W aplikacji WinForms reszta metody zostanie zakończona w tym samym wątku (wątek interfejsu użytkownika). Jest Task.Yield()to rzeczywiście przydatne w przypadkach, w których chcesz mieć pewność, że zwrócone produkty Tasknie zostaną natychmiast ukończone po utworzeniu.
Theodor Zoulias