Wykorzystywanie wzorca asynchronicznego opartego na zadaniach

W przypadku korzystania ze wzorca asynchronicznego opartego na zadaniach (TAP) do pracy z operacjami asynchronicznymi można użyć wywołań zwrotnych, aby osiągnąć oczekiwanie bez blokowania. W przypadku zadań można to osiągnąć za pomocą metod takich jak Task.ContinueWith. Obsługa asynchroniczna oparta na języku ukrywa wywołania zwrotne, umożliwiając oczekiwanie na operacje asynchroniczne w normalnym przepływie sterowania, a kod generowany przez kompilator zapewnia tę samą obsługę na poziomie interfejsu API.

Wstrzymywanie wykonywania za pomocą funkcji Await

Możesz użyć słowa kluczowego await w języku C# i operatora Await w Visual Basic, aby asynchronicznie czekać Task i Task<TResult> obiekty. Gdy oczekujesz na element Task, await wyrażenie ma typ void. Gdy oczekujesz na element Task<TResult>, await wyrażenie ma typ TResult. Wyrażenie await musi występować wewnątrz treści metody asynchronicznej. (Te funkcje językowe zostały wprowadzone w programie .NET Framework 4.5).

W ramach okładek funkcja await instaluje wywołanie zwrotne zadania przy użyciu kontynuacji. To wywołanie zwrotne wznawia metodę asynchroniczną w momencie zawieszenia. Po wznowieniu metody asynchronicznej, jeśli oczekiwana operacja została ukończona pomyślnie i była elementem Task<TResult>, TResult zostanie zwrócony. Task Jeśli oczekiwana wartość lub Task<TResult> została zakończona w Canceled stanie, OperationCanceledException zgłaszany jest wyjątek. Jeśli oczekiwana wartość Task lub Task<TResult> została zakończona w Faulted stanie, zgłaszany jest wyjątek, który spowodował wystąpienie błędu. Może Task to spowodować błąd w wyniku wielu wyjątków, ale jest propagowany tylko jeden z tych wyjątków. Task.Exception Jednak właściwość zwraca AggregateException wyjątek zawierający wszystkie błędy.

Jeśli kontekst synchronizacji (SynchronizationContext obiekt) jest skojarzony z wątkiem, który wykonywał metodę asynchroniczną w czasie zawieszenia (na przykład jeśli SynchronizationContext.Current właściwość nie nulljest ), metoda asynchroniczna zostanie wznowiona w tym samym kontekście synchronizacji przy użyciu metody kontekstu Post . W przeciwnym razie opiera się na harmonogramie zadań (TaskScheduler obiekt), który był obecny w momencie zawieszenia. Zazwyczaj jest to domyślny harmonogram zadań (TaskScheduler.Default), który jest przeznaczony dla puli wątków. Ten harmonogram zadań określa, czy oczekiwana operacja asynchroniczna powinna zostać wznowiona, w którym została ukończona, czy też ma zostać zaplanowana ponowna operacja. Domyślny harmonogram zwykle umożliwia kontynuowanie działania w wątku, który zakończył oczekiwaną operację.

Gdy wywoływana jest metoda asynchroniczna, synchronicznie wykonuje treść funkcji aż do pierwszego wyrażenia await w oczekiwanym wystąpieniu, które nie zostało jeszcze ukończone, w którym wywołanie powróci do obiektu wywołującego. Jeśli metoda asynchroniczna nie zwraca voidwartości , Task zwracany jest obiekt lub Task<TResult> reprezentujący bieżące obliczenia. W metodzie asynchronicznej niepustej, jeśli napotkana jest instrukcja return lub osiągnięto koniec treści metody, zadanie jest wykonywane w RanToCompletion stanie końcowym. Jeśli nieobsługiwany wyjątek powoduje pozostawienie kontrolki w treści metody asynchronicznej, zadanie kończy się w Faulted stanie . Jeśli ten wyjątek to OperationCanceledException, zadanie zamiast tego kończy się w Canceled stanie . W ten sposób wynik lub wyjątek zostanie ostatecznie opublikowany.

Istnieje kilka ważnych odmian tego zachowania. Ze względu na wydajność, jeśli zadanie zostało już ukończone przez czas oczekiwania na zadanie, kontrolka nie zostanie zwrócona, a funkcja będzie nadal wykonywana. Ponadto powrót do oryginalnego kontekstu nie zawsze jest pożądanym zachowaniem i można go zmienić; opisano to bardziej szczegółowo w następnej sekcji.

Konfigurowanie zawieszenia i wznowienia przy użyciu funkcji Yield i ConfigureAwait

Kilka metod zapewnia większą kontrolę nad wykonywaniem metody asynchronicznej. Na przykład można użyć Task.Yield metody , aby wprowadzić punkt wydajności do metody asynchronicznej:

public class Task : …
{
    public static YieldAwaitable Yield();
    …
}

Jest to odpowiednik asynchronicznego publikowania lub planowania z powrotem do bieżącego kontekstu.

Task.Run(async delegate
{
    for(int i=0; i<1000000; i++)
    {
        await Task.Yield(); // fork the continuation into a separate work item
        ...
    }
});

Można również użyć Task.ConfigureAwait metody, aby lepiej kontrolować zawieszenie i wznowienie w metodzie asynchronicznej. Jak wspomniano wcześniej, domyślnie bieżący kontekst jest przechwytywany w czasie wstrzymania metody asynchronicznej, a przechwycony kontekst jest używany do wywoływania kontynuacji metody asynchronicznej po wznowieniu. W wielu przypadkach jest to dokładne zachowanie. W innych przypadkach możesz nie przejmować się kontekstem kontynuacji i można osiągnąć lepszą wydajność, unikając takich wpisów z powrotem do oryginalnego kontekstu. Aby to włączyć, użyj Task.ConfigureAwait metody , aby poinformować operację await, aby nie przechwycić i wznowić w kontekście, ale kontynuować wykonywanie wszędzie tam, gdzie operacja asynchroniczna, która była oczekiwana:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Anulowanie operacji asynchronicznej

Począwszy od programu .NET Framework 4, metody TAP obsługujące anulowanie zapewniają co najmniej jedno przeciążenie, które akceptuje token anulowania (CancellationToken obiekt).

Token anulowania jest tworzony za pośrednictwem źródła tokenu anulowania (CancellationTokenSource obiektu). Właściwość źródła Token zwraca token anulowania, który zostanie zasygnalizowany po wywołaniu metody źródła Cancel . Jeśli na przykład chcesz pobrać jedną stronę internetową i chcesz mieć możliwość anulowania operacji, utworzysz CancellationTokenSource obiekt, przekaż jego token do metody TAP, a następnie wywołaj metodę źródła Cancel , gdy wszystko będzie gotowe do anulowania operacji:

var cts = new CancellationTokenSource();
string result = await DownloadStringTaskAsync(url, cts.Token);
… // at some point later, potentially on another thread
cts.Cancel();

Aby anulować wiele wywołań asynchronicznych, możesz przekazać ten sam token do wszystkich wywołań:

var cts = new CancellationTokenSource();
    IList<string> results = await Task.WhenAll(from url in urls select DownloadStringTaskAsync(url, cts.Token));
    // at some point later, potentially on another thread
    …
    cts.Cancel();

Możesz też przekazać ten sam token do selektywnego podzestawu operacji:

var cts = new CancellationTokenSource();
    byte [] data = await DownloadDataAsync(url, cts.Token);
    await SaveToDiskAsync(outputPath, data, CancellationToken.None);
    … // at some point later, potentially on another thread
    cts.Cancel();

Ważne

Żądania anulowania mogą być inicjowane z dowolnego wątku.

Wartość można przekazać CancellationToken.None do dowolnej metody, która akceptuje token anulowania, aby wskazać, że anulowanie nigdy nie zostanie zażądane. CancellationToken.CanBeCanceled Powoduje to zwrócenie falsewłaściwości , a wywołana metoda może odpowiednio zoptymalizować. W celach testowych można również przekazać wstępnie anulowany token anulowania utworzony za pomocą konstruktora, który akceptuje wartość logiczną, aby wskazać, czy token powinien zostać uruchomiony w stanie już anulowanym, czy nie można anulować.

Takie podejście do anulowania ma kilka zalet:

  • Możesz przekazać ten sam token anulowania do dowolnej liczby operacji asynchronicznych i synchronicznych.

  • To samo żądanie anulowania może być dystrybuowane do dowolnej liczby odbiorników.

  • Deweloper asynchronicznego interfejsu API ma pełną kontrolę nad tym, czy może zostać zażądane anulowanie i kiedy może zostać zastosowane.

  • Kod korzystający z interfejsu API może selektywnie określić asynchroniczne wywołania, do których będą propagowane żądania anulowania.

Postęp monitorowania

Niektóre metody asynchroniczne uwidaczniają postęp za pośrednictwem interfejsu postępu przekazanego do metody asynchronicznej. Rozważmy na przykład funkcję, która asynchronicznie pobiera ciąg tekstu, a po drodze podnosi postęp aktualizacji, które obejmują procent pobierania, który został ukończony do tej pory. Taką metodę można użyć w aplikacji Windows Presentation Foundation (WPF) w następujący sposób:

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtResult.Text = await DownloadStringTaskAsync(txtUrl.Text,
            new Progress<int>(p => pbDownloadProgress.Value = p));
    }
    finally { btnDownload.IsEnabled = true; }
}

Korzystanie z wbudowanych kombinatorów opartych na zadaniach

System.Threading.Tasks Przestrzeń nazw zawiera kilka metod komponowania i pracy z zadaniami.

Task.run

Klasa Task zawiera kilka Run metod, które pozwalają łatwo odciążać pracę jako pulę Task wątków lub Task<TResult> , na przykład:

public async void button1_Click(object sender, EventArgs e)
{
    textBox1.Text = await Task.Run(() =>
    {
        // … do compute-bound work here
        return answer;
    });
}

Niektóre z tych Run metod, takie jak Task.Run(Func<Task>) przeciążenie, istnieją jako skrócone dla TaskFactory.StartNew metody . To przeciążenie umożliwia użycie funkcji await w ramach odciążonej pracy, na przykład:

public async void button1_Click(object sender, EventArgs e)
{
    pictureBox1.Image = await Task.Run(async() =>
    {
        using(Bitmap bmp1 = await DownloadFirstImageAsync())
        using(Bitmap bmp2 = await DownloadSecondImageAsync())
        return Mashup(bmp1, bmp2);
    });
}

Takie przeciążenia są logicznie równoważne użyciu TaskFactory.StartNew metody w połączeniu z Unwrap metodą rozszerzenia w bibliotece równoległej zadań.

Task.FromResult

FromResult Użyj metody w scenariuszach, w których dane mogą być już dostępne i po prostu muszą zostać zwrócone z metody zwracanej przez zadanie, która została podniesiona do Task<TResult>metody :

public Task<int> GetValueAsync(string key)
{
    int cachedValue;
    return TryGetCachedValue(out cachedValue) ?
        Task.FromResult(cachedValue) :
        GetValueAsyncInternal();
}

private async Task<int> GetValueAsyncInternal(string key)
{
    …
}

Task.WhenAll

WhenAll Użyj metody , aby asynchronicznie czekać na wiele operacji asynchronicznych, które są reprezentowane jako zadania. Metoda ma wiele przeciążeń, które obsługują zestaw zadań niegenerycznych lub nieujednolity zestaw zadań ogólnych (na przykład asynchronicznie oczekujących na wiele operacji zwracanych przez pustą wartość lub asynchronicznie oczekujących na wiele metod zwracania wartości, w których każda wartość może mieć inny typ) i obsługiwać jednolity zestaw zadań ogólnych (takich jak asynchroniczne oczekiwanie na wiele TResultmetod zwracania).

Załóżmy, że chcesz wysyłać wiadomości e-mail do kilku klientów. Możesz nakładać się na wysyłanie komunikatów, aby nie czekać na ukończenie jednej wiadomości przed wysłaniem następnej. Możesz również dowiedzieć się, kiedy operacje wysyłania zostały ukończone i czy wystąpiły jakiekolwiek błędy:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
await Task.WhenAll(asyncOps);

Ten kod nie obsługuje jawnie wyjątków, które mogą wystąpić, ale umożliwia propagowanie wyjątków z await zadania wynikowego z .WhenAll Aby obsłużyć wyjątki, możesz użyć kodu, takiego jak:

IEnumerable<Task> asyncOps = from addr in addrs select SendMailAsync(addr);
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    ...
}

W takim przypadku, jeśli jakakolwiek operacja asynchroniczna zakończy się niepowodzeniem AggregateException , wszystkie wyjątki zostaną skonsolidowane w wyjątku, który jest przechowywany w Task obiekcie zwracanym z WhenAll metody . Jednak tylko jeden z tych wyjątków jest propagowany przez await słowo kluczowe . Jeśli chcesz zbadać wszystkie wyjątki, możesz ponownie napisać poprzedni kod w następujący sposób:

Task [] asyncOps = (from addr in addrs select SendMailAsync(addr)).ToArray();
try
{
    await Task.WhenAll(asyncOps);
}
catch(Exception exc)
{
    foreach(Task faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

Rozważmy przykład pobierania wielu plików z sieci Web asynchronicznie. W takim przypadku wszystkie operacje asynchroniczne mają homogeniczne typy wyników i łatwo jest uzyskać dostęp do wyników:

string [] pages = await Task.WhenAll(
    from url in urls select DownloadStringTaskAsync(url));

Możesz użyć tych samych technik obsługi wyjątków omówionych w poprzednim scenariuszu zwracania pustki:

Task<string> [] asyncOps =
    (from url in urls select DownloadStringTaskAsync(url)).ToArray();
try
{
    string [] pages = await Task.WhenAll(asyncOps);
    ...
}
catch(Exception exc)
{
    foreach(Task<string> faulted in asyncOps.Where(t => t.IsFaulted))
    {
        … // work with faulted and faulted.Exception
    }
}

Task.WhenAny

Możesz użyć WhenAny metody , aby asynchronicznie czekać tylko na jedną z wielu operacji asynchronicznych reprezentowanych jako zadania do wykonania. Ta metoda obsługuje cztery główne przypadki użycia:

  • Nadmiarowość: wykonanie operacji wiele razy i wybranie tej, która zostanie ukończona jako pierwsza (na przykład skontaktowanie się z wieloma usługami internetowymi ofert giełdowych, które spowodują wygenerowanie jednego wyniku i wybranie tej, która zakończy się najszybciej).

  • Przeplatanie: uruchamianie wielu operacji i oczekiwanie na ich zakończenie, ale przetwarzanie ich po zakończeniu.

  • Ograniczanie przepustowości: zezwalanie na rozpoczęcie dodatkowych operacji w miarę ukończenia innych operacji. Jest to rozszerzenie scenariusza przeplatania.

  • Wczesne ratowanie: na przykład operacja reprezentowana przez zadanie t1 może być zgrupowana w WhenAny zadaniu z innym zadaniem t2 i można czekać na WhenAny zadanie. Zadanie t2 może reprezentować limit czasu lub anulowanie lub inny sygnał, który powoduje WhenAny zakończenie zadania przed ukończeniem t1.

Nadmiarowość

Rozważ przypadek, w którym chcesz podjąć decyzję o tym, czy kupić akcje. Istnieje kilka zaufanych usług internetowych rekomendacji, ale w zależności od codziennego obciążenia każda usługa może działać wolno w różnych momentach. Możesz użyć metody , aby otrzymywać powiadomienie po zakończeniu WhenAny dowolnej operacji:

var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol),
    GetBuyRecommendation2Async(symbol),
    GetBuyRecommendation3Async(symbol)
};
Task<bool> recommendation = await Task.WhenAny(recommendations);
if (await recommendation) BuyStock(symbol);

W przeciwieństwie do WhenAllmetody , która zwraca niezapisane wyniki wszystkich zadań zakończonych pomyślnie, WhenAny zwraca zadanie, które zostało ukończone. Jeśli zadanie zakończy się niepowodzeniem, ważne jest, aby wiedzieć, że nie powiodło się, a jeśli zadanie zakończy się pomyślnie, ważne jest, aby wiedzieć, z którym zadaniem jest skojarzona wartość zwracana. W związku z tym musisz uzyskać dostęp do wyniku zwróconego zadania lub poczekać dalej, jak pokazano w tym przykładzie.

Podobnie jak w przypadku WhenAllprogramu, musisz mieć możliwość obsługi wyjątków. Ponieważ zadanie ukończone zostanie zwrócone, możesz poczekać na propagację zwróconych zadań i odpowiednio je rozpropagować, try/catch na przykład:

Task<bool> [] recommendations = …;
while(recommendations.Count > 0)
{
    Task<bool> recommendation = await Task.WhenAny(recommendations);
    try
    {
        if (await recommendation) BuyStock(symbol);
        break;
    }
    catch(WebException exc)
    {
        recommendations.Remove(recommendation);
    }
}

Ponadto nawet jeśli pierwsze zadanie zakończy się pomyślnie, kolejne zadania mogą zakończyć się niepowodzeniem. W tym momencie masz kilka opcji obsługi wyjątków: możesz poczekać, aż wszystkie uruchomione zadania zostały ukończone, w takim przypadku można użyć WhenAll metody lub zdecydować, że wszystkie wyjątki są ważne i muszą być rejestrowane. W tym celu można użyć kontynuacji, aby otrzymywać powiadomienie, gdy zadania zostały wykonane asynchronicznie:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => { if (t.IsFaulted) Log(t.Exception); });
}

or:

foreach(Task recommendation in recommendations)
{
    var ignored = recommendation.ContinueWith(
        t => Log(t.Exception), TaskContinuationOptions.OnlyOnFaulted);
}

a nawet:

private static async void LogCompletionIfFailed(IEnumerable<Task> tasks)
{
    foreach(var task in tasks)
    {
        try { await task; }
        catch(Exception exc) { Log(exc); }
    }
}
…
LogCompletionIfFailed(recommendations);

Na koniec możesz anulować wszystkie pozostałe operacje:

var cts = new CancellationTokenSource();
var recommendations = new List<Task<bool>>()
{
    GetBuyRecommendation1Async(symbol, cts.Token),
    GetBuyRecommendation2Async(symbol, cts.Token),
    GetBuyRecommendation3Async(symbol, cts.Token)
};

Task<bool> recommendation = await Task.WhenAny(recommendations);
cts.Cancel();
if (await recommendation) BuyStock(symbol);

Przeplataniem

Rozważ przypadek, w którym pobierasz obrazy z Internetu i przetwarzasz każdy obraz (na przykład dodanie obrazu do kontrolki interfejsu użytkownika). Obrazy są przetwarzane sekwencyjnie w wątku interfejsu użytkownika, ale chcesz pobrać obrazy tak współbieżnie, jak to możliwe. Ponadto nie chcesz przechowywać dodawania obrazów do interfejsu użytkownika, dopóki nie zostaną pobrane. Zamiast tego chcesz dodać je w miarę ich ukończenia.

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

Można również zastosować przeplatanie do scenariusza obejmującego przetwarzanie intensywnie obliczeniowe na ThreadPool pobranych obrazach, na przykład:

List<Task<Bitmap>> imageTasks =
    (from imageUrl in urls select GetBitmapAsync(imageUrl)
         .ContinueWith(t => ConvertImage(t.Result)).ToList();
while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch{}
}

Ograniczanie przepływności

Rozważmy przykład przeplatania, z tą różnicą, że użytkownik pobiera tak wiele obrazów, że pobieranie musi być ograniczone; na przykład chcesz współbieżnie wykonać tylko określoną liczbę pobrań. Aby to osiągnąć, możesz uruchomić podzbiór operacji asynchronicznych. Po zakończeniu operacji możesz uruchomić dodatkowe operacje, aby wykonać następujące operacje:

const int CONCURRENCY_LEVEL = 15;
Uri [] urls = …;
int nextIndex = 0;
var imageTasks = new List<Task<Bitmap>>();
while(nextIndex < CONCURRENCY_LEVEL && nextIndex < urls.Length)
{
    imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
    nextIndex++;
}

while(imageTasks.Count > 0)
{
    try
    {
        Task<Bitmap> imageTask = await Task.WhenAny(imageTasks);
        imageTasks.Remove(imageTask);

        Bitmap image = await imageTask;
        panel.AddImage(image);
    }
    catch(Exception exc) { Log(exc); }

    if (nextIndex < urls.Length)
    {
        imageTasks.Add(GetBitmapAsync(urls[nextIndex]));
        nextIndex++;
    }
}

Wczesne ratowanie

Należy wziąć pod uwagę, że oczekujesz asynchronicznie na ukończenie operacji, jednocześnie odpowiadając na żądanie anulowania użytkownika (na przykład użytkownik kliknął przycisk anuluj). Poniższy kod ilustruje ten scenariusz:

private CancellationTokenSource m_cts;

public void btnCancel_Click(object sender, EventArgs e)
{
    if (m_cts != null) m_cts.Cancel();
}

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();
    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        if (imageDownload.IsCompleted)
        {
            Bitmap image = await imageDownload;
            panel.AddImage(image);
        }
        else imageDownload.ContinueWith(t => Log(t));
    }
    finally { btnRun.Enabled = true; }
}

private static async Task UntilCompletionOrCancellation(
    Task asyncOp, CancellationToken ct)
{
    var tcs = new TaskCompletionSource<bool>();
    using(ct.Register(() => tcs.TrySetResult(true)))
        await Task.WhenAny(asyncOp, tcs.Task);
    return asyncOp;
}

Ta implementacja ponownie włącza interfejs użytkownika natychmiast po podjęciu decyzji o ratowaniu, ale nie anuluje podstawowych operacji asynchronicznych. Inną alternatywą byłoby anulowanie oczekujących operacji po podjęciu decyzji o ratowaniu, ale nie ponownego opublikowania interfejsu użytkownika do momentu zakończenia operacji, potencjalnie z powodu zakończenia wczesnego z powodu żądania anulowania:

private CancellationTokenSource m_cts;

public async void btnRun_Click(object sender, EventArgs e)
{
    m_cts = new CancellationTokenSource();

    btnRun.Enabled = false;
    try
    {
        Task<Bitmap> imageDownload = GetBitmapAsync(txtUrl.Text, m_cts.Token);
        await UntilCompletionOrCancellation(imageDownload, m_cts.Token);
        Bitmap image = await imageDownload;
        panel.AddImage(image);
    }
    catch(OperationCanceledException) {}
    finally { btnRun.Enabled = true; }
}

Innym przykładem wczesnego ratowania jest użycie WhenAny metody w połączeniu z metodą, zgodnie z Delay opisem w następnej sekcji.

Task.Delay

Możesz użyć Task.Delay metody , aby wprowadzić wstrzymanie do wykonania metody asynchronicznej. Jest to przydatne w przypadku wielu rodzajów funkcji, w tym tworzenia pętli sondowania i opóźniania obsługi danych wejściowych użytkownika przez wstępnie określony okres czasu. Metoda może być również przydatna Task.Delay w połączeniu z implementacją Task.WhenAny limitów czasu dla oczekujących.

Jeśli zadanie będące częścią większej operacji asynchronicznej (na przykład usługa internetowa ASP.NET) trwa zbyt długo, ogólna operacja może ulec awarii, zwłaszcza jeśli kiedykolwiek zakończy się niepowodzeniem. Z tego powodu ważne jest, aby mieć możliwość przekroczenia limitu czasu podczas oczekiwania na operację asynchroniczną. Metody synchroniczne Task.Wait, Task.WaitAlli Task.WaitAny akceptują wartości limitu czasu, ale odpowiednie/TaskFactory.ContinueWhenAnyTaskFactory.ContinueWhenAlli wymienione Task.WhenAll/Task.WhenAny wcześniej metody nie. Zamiast tego możesz użyć funkcji Task.Delay i Task.WhenAny w połączeniu, aby zaimplementować limit czasu.

Na przykład w aplikacji interfejsu użytkownika załóżmy, że chcesz pobrać obraz i wyłączyć interfejs użytkownika podczas pobierania obrazu. Jeśli jednak pobieranie trwa zbyt długo, chcesz ponownie włączyć interfejs użytkownika i odrzucić pobieranie:

public async void btnDownload_Click(object sender, EventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        Task<Bitmap> download = GetBitmapAsync(url);
        if (download == await Task.WhenAny(download, Task.Delay(3000)))
        {
            Bitmap bmp = await download;
            pictureBox.Image = bmp;
            status.Text = "Downloaded";
        }
        else
        {
            pictureBox.Image = null;
            status.Text = "Timed out";
            var ignored = download.ContinueWith(
                t => Trace("Task finally completed"));
        }
    }
    finally { btnDownload.Enabled = true; }
}

To samo dotyczy wielu pobrań, ponieważ WhenAll zwraca zadanie:

public async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.Enabled = false;
    try
    {
        Task<Bitmap[]> downloads =
            Task.WhenAll(from url in urls select GetBitmapAsync(url));
        if (downloads == await Task.WhenAny(downloads, Task.Delay(3000)))
        {
            foreach(var bmp in downloads.Result) panel.AddImage(bmp);
            status.Text = "Downloaded";
        }
        else
        {
            status.Text = "Timed out";
            downloads.ContinueWith(t => Log(t));
        }
    }
    finally { btnDownload.Enabled = true; }
}

Kompilowanie kombinatorów opartych na zadaniach

Ponieważ zadanie jest w stanie całkowicie reprezentować operację asynchroniczną i zapewnić synchroniczne i asynchroniczne możliwości łączenia z operacją, pobierania jej wyników itd., można tworzyć przydatne biblioteki kombinatorów tworzących zadania do tworzenia większych wzorców. Jak wspomniano w poprzedniej sekcji, platforma .NET zawiera kilka wbudowanych kombinatorów, ale można również tworzyć własne. W poniższych sekcjach przedstawiono kilka przykładów potencjalnych metod i typów kombinatora.

RetryOnFault

W wielu sytuacjach można ponowić próbę wykonania operacji, jeśli poprzednia próba zakończy się niepowodzeniem. W przypadku kodu synchronicznego można utworzyć metodę pomocnika, taką jak RetryOnFault w poniższym przykładzie, aby to osiągnąć:

public static T RetryOnFault<T>(
    Func<T> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return function(); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

Możesz utworzyć niemal identyczną metodę pomocnika dla operacji asynchronicznych implementowanych za pomocą funkcji TAP, a tym samym zwrócić zadania:

public static async Task<T> RetryOnFault<T>(
    Func<Task<T>> function, int maxTries)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries-1) throw; }
    }
    return default(T);
}

Następnie można użyć tego kombinatora, aby zakodować ponawianie prób w logice aplikacji; na przykład:

// Download the URL, trying up to three times in case of failure
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3);

Możesz jeszcze bardziej rozszerzyć RetryOnFault funkcję. Na przykład funkcja może zaakceptować kolejną Func<Task> , która zostanie wywołana między ponowną próbą, aby określić, kiedy ponowić próbę wykonania operacji, na przykład:

public static async Task<T> RetryOnFault<T>(
    Func<Task<T>> function, int maxTries, Func<Task> retryWhen)
{
    for(int i=0; i<maxTries; i++)
    {
        try { return await function().ConfigureAwait(false); }
        catch { if (i == maxTries-1) throw; }
        await retryWhen().ConfigureAwait(false);
    }
    return default(T);
}

Następnie możesz użyć funkcji w następujący sposób, aby poczekać na sekundę przed ponowieniu próby wykonania operacji:

// Download the URL, trying up to three times in case of failure,
// and delaying for a second between retries
string pageContents = await RetryOnFault(
    () => DownloadStringTaskAsync(url), 3, () => Task.Delay(1000));

NeedOnlyOne

Czasami można skorzystać z nadmiarowości, aby poprawić opóźnienie operacji i szanse na powodzenie. Rozważ wiele usług internetowych, które dostarczają oferty giełdowe, ale o różnych porach dnia każda usługa może zapewnić różne poziomy czasu jakości i odpowiedzi. Aby radzić sobie z tymi fluktuacjami, możesz wysyłać żądania do wszystkich usług internetowych, a gdy tylko otrzymasz odpowiedź od jednego, anuluj pozostałe żądania. Możesz zaimplementować funkcję pomocnika, aby ułatwić implementację tego wspólnego wzorca uruchamiania wielu operacji, oczekiwania na dowolne operacje, a następnie anulowania reszty. Funkcja NeedOnlyOne w poniższym przykładzie ilustruje ten scenariusz:

public static async Task<T> NeedOnlyOne(
    params Func<CancellationToken,Task<T>> [] functions)
{
    var cts = new CancellationTokenSource();
    var tasks = (from function in functions
                 select function(cts.Token)).ToArray();
    var completed = await Task.WhenAny(tasks).ConfigureAwait(false);
    cts.Cancel();
    foreach(var task in tasks)
    {
        var ignored = task.ContinueWith(
            t => Log(t), TaskContinuationOptions.OnlyOnFaulted);
    }
    return completed;
}

Następnie możesz użyć tej funkcji w następujący sposób:

double currentPrice = await NeedOnlyOne(
    ct => GetCurrentPriceFromServer1Async("msft", ct),
    ct => GetCurrentPriceFromServer2Async("msft", ct),
    ct => GetCurrentPriceFromServer3Async("msft", ct));

Operacje przeplatane

Istnieje potencjalny problem z wydajnością przy użyciu WhenAny metody do obsługi scenariusza przeplatania podczas pracy z dużymi zestawami zadań. Każde wywołanie powoduje WhenAny zarejestrowanie kontynuacji przy użyciu każdego zadania. W przypadku N liczby zadań powoduje to kontynuacje O(N2) utworzone w okresie istnienia operacji przeplatania. Jeśli pracujesz z dużym zestawem zadań, możesz użyć kombinatora (Interleaved w poniższym przykładzie), aby rozwiązać problem z wydajnością:

static IEnumerable<Task<T>> Interleaved<T>(IEnumerable<Task<T>> tasks)
{
    var inputTasks = tasks.ToList();
    var sources = (from _ in Enumerable.Range(0, inputTasks.Count)
                   select new TaskCompletionSource<T>()).ToList();
    int nextTaskIndex = -1;
    foreach (var inputTask in inputTasks)
    {
        inputTask.ContinueWith(completed =>
        {
            var source = sources[Interlocked.Increment(ref nextTaskIndex)];
            if (completed.IsFaulted)
                source.TrySetException(completed.Exception.InnerExceptions);
            else if (completed.IsCanceled)
                source.TrySetCanceled();
            else
                source.TrySetResult(completed.Result);
        }, CancellationToken.None,
           TaskContinuationOptions.ExecuteSynchronously,
           TaskScheduler.Default);
    }
    return from source in sources
           select source.Task;
}

Następnie można użyć kombinatora, aby przetworzyć wyniki zadań podczas ich wykonywania; na przykład:

IEnumerable<Task<int>> tasks = ...;
foreach(var task in Interleaved(tasks))
{
    int result = await task;
    …
}

WhenAllOrFirstException

W niektórych scenariuszach punktowych/zbierających możesz poczekać na wszystkie zadania w zestawie, chyba że wystąpi jeden z nich błędów, w takim przypadku chcesz przestać czekać natychmiast po wystąpieniu wyjątku. Można to osiągnąć za pomocą metody kombinatora, takiej jak WhenAllOrFirstException w poniższym przykładzie:

public static Task<T[]> WhenAllOrFirstException<T>(IEnumerable<Task<T>> tasks)
{
    var inputs = tasks.ToList();
    var ce = new CountdownEvent(inputs.Count);
    var tcs = new TaskCompletionSource<T[]>();

    Action<Task> onCompleted = (Task completed) =>
    {
        if (completed.IsFaulted)
            tcs.TrySetException(completed.Exception.InnerExceptions);
        if (ce.Signal() && !tcs.Task.IsCompleted)
            tcs.TrySetResult(inputs.Select(t => t.Result).ToArray());
    };

    foreach (var t in inputs) t.ContinueWith(onCompleted);
    return tcs.Task;
}

Tworzenie struktur danych opartych na zadaniach

Oprócz możliwości tworzenia niestandardowych kombinatorów opartych na zadaniach, posiadania struktury danych w Task systemie i Task<TResult> reprezentującej zarówno wyniki operacji asynchronicznej, jak i niezbędnej synchronizacji do sprzężenia z nim sprawia, że jest to zaawansowany typ, na którym można tworzyć niestandardowe struktury danych do użycia w scenariuszach asynchronicznych.

AsyncCache

Jednym z ważnych aspektów zadania jest to, że może zostać przekazany wielu konsumentom, z których wszyscy mogą oczekiwać, zarejestrować kontynuacje w nim, uzyskać jego wynik lub wyjątki (w przypadku Task<TResult>), itd. To sprawia, że Task i Task<TResult> doskonale nadaje się do użycia w infrastrukturze asynchronicznej buforowania. Oto przykład małej, ale wydajnej asynchronicznej pamięci podręcznej opartej Task<TResult>na :

public class AsyncCache<TKey, TValue>
{
    private readonly Func<TKey, Task<TValue>> _valueFactory;
    private readonly ConcurrentDictionary<TKey, Lazy<Task<TValue>>> _map;

    public AsyncCache(Func<TKey, Task<TValue>> valueFactory)
    {
        if (valueFactory == null) throw new ArgumentNullException("valueFactory");
        _valueFactory = valueFactory;
        _map = new ConcurrentDictionary<TKey, Lazy<Task<TValue>>>();
    }

    public Task<TValue> this[TKey key]
    {
        get
        {
            if (key == null) throw new ArgumentNullException("key");
            return _map.GetOrAdd(key, toAdd =>
                new Lazy<Task<TValue>>(() => _valueFactory(toAdd))).Value;
        }
    }
}

Klasa AsyncCache<TKey,TValue> przyjmuje jako delegata do konstruktora funkcję, która przyjmuje TKey element i zwraca wartość Task<TResult>. Wszystkie wcześniej dostępne wartości z pamięci podręcznej są przechowywane w słowniku wewnętrznym i AsyncCache zapewnia, że tylko jedno zadanie jest generowane dla każdego klucza, nawet jeśli pamięć podręczna jest uzyskiwana współbieżnie.

Na przykład można utworzyć pamięć podręczną dla pobranych stron internetowych:

private AsyncCache<string,string> m_webPages =
    new AsyncCache<string,string>(DownloadStringTaskAsync);

Następnie możesz użyć tej pamięci podręcznej w metodach asynchronicznych, gdy potrzebujesz zawartości strony internetowej. Klasa AsyncCache zapewnia, że pobierasz jak najmniej stron i buforuje wyniki.

private async void btnDownload_Click(object sender, RoutedEventArgs e)
{
    btnDownload.IsEnabled = false;
    try
    {
        txtContents.Text = await m_webPages["https://www.microsoft.com"];
    }
    finally { btnDownload.IsEnabled = true; }
}

AsyncProducerConsumerCollection

Za pomocą zadań można również tworzyć struktury danych do koordynowania działań asynchronicznych. Rozważmy jeden z klasycznych wzorców projektowania równoległego: producent/konsument. W tym wzorcu producenci generują dane używane przez konsumentów, a producenci i konsumenci mogą działać równolegle. Na przykład konsument przetwarza element 1, który został wcześniej wygenerowany przez producenta, który produkuje teraz element 2. W przypadku wzorca producenta/konsumenta niezmiennie potrzebna jest struktura danych do przechowywania pracy utworzonej przez producentów, dzięki czemu konsumenci mogą otrzymywać powiadomienia o nowych danych i znajdować je, gdy są dostępne.

Oto prosta struktura danych oparta na zadaniach, która umożliwia użycie metod asynchronicznych jako producentów i konsumentów:

public class AsyncProducerConsumerCollection<T>
{
    private readonly Queue<T> m_collection = new Queue<T>();
    private readonly Queue<TaskCompletionSource<T>> m_waiting =
        new Queue<TaskCompletionSource<T>>();

    public void Add(T item)
    {
        TaskCompletionSource<T> tcs = null;
        lock (m_collection)
        {
            if (m_waiting.Count > 0) tcs = m_waiting.Dequeue();
            else m_collection.Enqueue(item);
        }
        if (tcs != null) tcs.TrySetResult(item);
    }

    public Task<T> Take()
    {
        lock (m_collection)
        {
            if (m_collection.Count > 0)
            {
                return Task.FromResult(m_collection.Dequeue());
            }
            else
            {
                var tcs = new TaskCompletionSource<T>();
                m_waiting.Enqueue(tcs);
                return tcs.Task;
            }
        }
    }
}

Po wykonaniu tej struktury danych można napisać kod, taki jak:

private static AsyncProducerConsumerCollection<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.Take();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Add(data);
}

System.Threading.Tasks.Dataflow Przestrzeń nazw zawiera BufferBlock<T> typ, którego można użyć w podobny sposób, ale bez konieczności tworzenia niestandardowego typu kolekcji:

private static BufferBlock<int> m_data = …;
…
private static async Task ConsumerAsync()
{
    while(true)
    {
        int nextItem = await m_data.ReceiveAsync();
        ProcessNextItem(nextItem);
    }
}
…
private static void Produce(int data)
{
    m_data.Post(data);
}

Uwaga

System.Threading.Tasks.Dataflow Przestrzeń nazw jest dostępna jako pakiet NuGet. Aby zainstalować zestaw zawierający System.Threading.Tasks.Dataflow przestrzeń nazw, otwórz projekt w programie Visual Studio, wybierz pozycję Zarządzaj pakietami NuGet z menu Project i wyszukaj pakiet w trybie online System.Threading.Tasks.Dataflow .

Zobacz też