Использование асинхронного шаблона, основанного на задачахConsuming the Task-based Asynchronous Pattern

При работе асинхронными операциями с использованием асинхронного шаблона, основанного на задачах, можно использовать обратные вызовы для реализации неблокирующего ожидания.When you use the Task-based Asynchronous Pattern (TAP) to work with asynchronous operations, you can use callbacks to achieve waiting without blocking. Для задач это достигается с помощью таких методов, как Task.ContinueWith.For tasks, this is achieved through methods such as Task.ContinueWith. Поддержка асинхронных операций на основе языка скрывает обратные вызовы, разрешая асинхронным операциям находиться в режиме ожидания в нормальном потоке управления, а код, созданный компилятором, предоставляет поддержку на том же уровне API.Language-based asynchronous support hides callbacks by allowing asynchronous operations to be awaited within normal control flow, and compiler-generated code provides this same API-level support.

Приостановление выполнения с помощью AwaitSuspending Execution with Await

Начиная с версии .NET Framework 4.5 для асинхронного ожидания объектов Task и Task<TResult> можно использовать ключевое слово await (в C#) и оператор Await (в Visual Basic).Starting with the .NET Framework 4.5, you can use the await keyword in C# and the Await Operator in Visual Basic to asynchronously await Task and Task<TResult> objects. Когда вы ожидаете Task, выражение await имеет тип void.When you're awaiting a Task, the await expression is of type void. Когда вы ожидаете Task<TResult>, выражение await имеет тип TResult.When you're awaiting a Task<TResult>, the await expression is of type TResult. Выражение await должно находиться в теле асинхронного метода.An await expression must occur inside the body of an asynchronous method. Дополнительные сведения о поддержке языков C# и Visual Basic в .NET Framework 4.5 см. в спецификациях языка C# и Visual Basic.For more information about C# and Visual Basic language support in the .NET Framework 4.5, see the C# and Visual Basic language specifications.

На самом деле функция ожидания реализуется с помощью установки обратного вызова для задачи с помощью продолжения.Under the covers, the await functionality installs a callback on the task by using a continuation. Этот обратный вызов возобновляет асинхронный методы в точке остановки.This callback resumes the asynchronous method at the point of suspension. При возобновлении асинхронного метода, если ожидаемая операция была завершена успешно и имела тип Task<TResult>, возвращается ее значение TResult.When the asynchronous method is resumed, if the awaited operation completed successfully and was a Task<TResult>, its TResult is returned. Если ожидаемая операция Task или Task<TResult> завершилась с состоянием Canceled, создается исключение OperationCanceledException.If the Task or Task<TResult> that was awaited ended in the Canceled state, an OperationCanceledException exception is thrown. Если ожидаемая операция Task или Task<TResult> завершилась с состоянием Faulted, создается вызвавшее эту проблему исключение.If the Task or Task<TResult> that was awaited ended in the Faulted state, the exception that caused it to fault is thrown. Task может завершиться с ошибкой из-за нескольких исключений, но распространяется только одно из этих исключений.A Task can fault as a result of multiple exceptions, but only one of these exceptions is propagated. Тем не менее, свойство Task.Exception возвращает исключение AggregateException с полным списком ошибок.However, the Task.Exception property returns an AggregateException exception that contains all the errors.

Если контекст синхронизации (объект SynchronizationContext) связан с потоком, который во время приостановки выполнял асинхронный метод (например, если свойство SynchronizationContext.Current имеет значение, отличное от null), асинхронный метод возобновляется в том же контексте синхронизации, для чего вызывается метод Post этого контекста.If a synchronization context (SynchronizationContext object) is associated with the thread that was executing the asynchronous method at the time of suspension (for example, if the SynchronizationContext.Current property is not null), the asynchronous method resumes on that same synchronization context by using the context’s Post method. В противном случае он полагается на планировщик задач (объект TaskScheduler), который использовался в момент приостановки.Otherwise, it relies on the task scheduler (TaskScheduler object) that was current at the time of suspension. Обычно это планировщик по умолчанию (TaskScheduler.Default), который нацелен на пул потоков.Typically, this is the default task scheduler (TaskScheduler.Default), which targets the thread pool. Этот планировщик задач определяет, следует ли возобновить приостановленную асинхронную операцию в тот момент, в который она была завершена, или следует ли запланировать возобновление.This task scheduler determines whether the awaited asynchronous operation should resume where it completed or whether the resumption should be scheduled. Планировщик по умолчанию обычно разрешает продолжение выполнения в потоке, который был завершен операцией.The default scheduler typically allows the continuation to run on the thread that the awaited operation completed.

При вызове асинхронного метода он синхронно выполняет тело функции до первого выражения await для ожидаемого экземпляра, которое еще не было завершено, и в этот момент управление передается вызывающему объекту.When an asynchronous method is called, it synchronously executes the body of the function up until the first await expression on an awaitable instance that has not yet completed, at which point the invocation returns to the caller. Если асинхронный метод не возвращает void, в качестве представления текущего вычисления возвращается объект Task или Task<TResult>.If the asynchronous method does not return void, a Task or Task<TResult> object is returned to represent the ongoing computation. В асинхронном методе, который возвращает значение, отличное от void, при обнаружении выражения return или при достижении окончания метода задача завершается в конечном состоянии RanToCompletion.In a non-void asynchronous method, if a return statement is encountered or the end of the method body is reached, the task is completed in the RanToCompletion final state. Если асинхронный метод теряет управление из-за необработанного исключения, задача завершается в состоянии Faulted.If an unhandled exception causes control to leave the body of the asynchronous method, the task ends in the Faulted state. Если же это исключение является OperationCanceledException, задача завершается в состоянии Canceled.If that exception is an OperationCanceledException, the task instead ends in the Canceled state. Таким образом, результат или исключение в конечном счете будут сформированы.In this manner, the result or exception is eventually published.

Существует несколько важных вариантов такого поведения.There are several important variations of this behavior. Для повышения производительности, если к моменту ожидания задачи оказывается, что задача уже завершена, то управление не освобождается и функция продолжает выполнение.For performance reasons, if a task has already completed by the time the task is awaited, control is not yielded, and the function continues to execute. Кроме того, возврат к исходному контексту не всегда желателен, и такое поведение можно изменить. Подробное описание приведено в следующем разделе.Additionally, returning to the original context isn't always the desired behavior and can be changed; this is described in more detail in the next section.

Настройка приостановки и возобновления с помощью Yield и ConfigureAwaitConfiguring Suspension and Resumption with Yield and ConfigureAwait

Существуют методы, которые позволяют получить больший контроль над выполнением асинхронного метода.Several methods provide more control over an asynchronous method’s execution. Например, вы можете использовать метод Task.Yield для внедрения точки приостановки в асинхронный метод:For example, you can use the Task.Yield method to introduce a yield point into the asynchronous method:

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

Это аналогично асинхронному размещению или планированию возврата в текущий контекст.This is equivalent to asynchronously posting or scheduling back to the current context.

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

Также можно использовать метод Task.ConfigureAwait для более точного контроля над приостановкой и возобновлением в асинхронном методе.You can also use the Task.ConfigureAwait method for better control over suspension and resumption in an asynchronous method. Как упоминалось ранее, по умолчанию текущий контекст записывается в момент приостановки асинхронного метода и используется для вызова продолжения асинхронного метода при возобновлении.As mentioned previously, by default, the current context is captured at the time an asynchronous method is suspended, and that captured context is used to invoke the asynchronous method’s continuation upon resumption. Во многих случаях это именно то поведение, к которому вы стремитесь.In many cases, this is the exact behavior you want. В других случаях можно не заботиться о контексте продолжения. Для повышения производительности нужно избегать подобного размещения обратно в исходный контекст.In other cases, you may not care about the continuation context, and you can achieve better performance by avoiding such posts back to the original context. Для этого воспользуйтесь методом Task.ConfigureAwait, чтобы сообщить операции await о том, что перехватывать и возобновлять контекст не нужно, и вместо этого необходимо продолжить выполнение в той точке, в которой завершилась ожидаемая асинхронная операция.To enable this, use the Task.ConfigureAwait method to inform the await operation not to capture and resume on the context, but to continue execution wherever the asynchronous operation that was being awaited completed:

await someTask.ConfigureAwait(continueOnCapturedContext:false);

Отмена асинхронной операцииCanceling an Asynchronous Operation

Начиная с .NET Framework 4, методы TAP, которые поддерживают отмену, предоставляют по крайней мере одну перегрузку, которая принимает маркер отмены (объект CancellationToken).Starting with the .NET Framework 4, TAP methods that support cancellation provide at least one overload that accepts a cancellation token (CancellationToken object).

Маркер отмены создается с помощью источника маркеров отмены (объект CancellationTokenSource).A cancellation token is created through a cancellation token source (CancellationTokenSource object). Свойство Token источника возвращает маркер отмены, который будет передаваться при вызове метода Cancel источника.The source’s Token property returns the cancellation token that will be signaled when the source’s Cancel method is called. Например, если вы хотите скачать одну веб-страницу и при этом иметь возможность отменить операцию, создайте объект CancellationTokenSource, передайте его маркер методу TAP и вызовите метод источника Cancel, когда нужно будет отменить операцию.For example, if you want to download a single webpage and you want to be able to cancel the operation, you create a CancellationTokenSource object, pass its token to the TAP method, and then call the source’s Cancel method when you're ready to cancel the operation:

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

Чтобы отменить несколько асинхронных вызовов, можно передать один и тот же маркер всем вызовам.To cancel multiple asynchronous invocations, you can pass the same token to all invocations:

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

Также можно передать один и тот же маркер выбранному подмножеству операций.Or, you can pass the same token to a selective subset of operations:

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();

Запрос на отмену может быть запущен из любого потока.Cancellation requests may be initiated from any thread.

Значение CancellationToken.None можно передать любому методу, который принимает маркер отмены. Это будет означать, что отмена никогда не будет запрашиваться.You can pass the CancellationToken.None value to any method that accepts a cancellation token to indicate that cancellation will never be requested. В результате свойство CancellationToken.CanBeCanceled будет возвращать false, и вызываемый метод сможет принять меры для оптимизации.This causes the CancellationToken.CanBeCanceled property to return false, and the called method can optimize accordingly. Для тестирования также можно передать маркер отмены, для которого уже была выполнена отмена. Этот маркер инициализируется с помощью конструктора, который принимает логическое значение, означающее, следует ли запустить маркер в уже отмененном или неотменяемом состоянии.For testing purposes, you can also pass in a pre-canceled cancellation token that is instantiated by using the constructor that accepts a Boolean value to indicate whether the token should start in an already-canceled or not-cancelable state.

У такого подхода к отмене есть несколько преимуществ.This approach to cancellation has several advantages:

  • Один и тот же маркер отмены можно передать в любое количество асинхронных и синхронных операций.You can pass the same cancellation token to any number of asynchronous and synchronous operations.

  • Один и тот же запрос отмены можно распространить на любое количество прослушивателей.The same cancellation request may be proliferated to any number of listeners.

  • Разработчик асинхронного интерфейса API имеет полный контроль над тем, можно ли разрешить запрос отмены и когда ее можно применить.The developer of the asynchronous API is in complete control of whether cancellation may be requested and when it may take effect.

  • Код, который использует этот интерфейс API, может выборочно определять асинхронные вызовы, на которые будут распространены запросы отмены.The code that consumes the API may selectively determine the asynchronous invocations that cancellation requests will be propagated to.

Наблюдение за ходом выполненияMonitoring Progress

Некоторые асинхронные методы предоставляют сведения о ходе выполнения с помощью интерфейса хода выполнения, который передается в асинхронный метод.Some asynchronous methods expose progress through a progress interface passed into the asynchronous method. Например, рассмотрим функцию, которая асинхронно загружает строку текста, одновременно обновляя сведения о ходе загрузки, которые включают долю уже загруженной части строки в процентах ко всей строке.For example, consider a function which asynchronously downloads a string of text, and along the way raises progress updates that include the percentage of the download that has completed thus far. Этот метод можно использовать в приложении WPF следующим образом.Such a method could be consumed in a Windows Presentation Foundation (WPF) application as follows:

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

Использование внутренних блоков объединения задачUsing the Built-in Task-based Combinators

В пространстве имен System.Threading.Tasks предусмотрено несколько способов объединять задачи и работать с ними.The System.Threading.Tasks namespace includes several methods for composing and working with tasks.

Task.RunTask.Run

Класс Task содержит несколько методов Run, которые позволяют легко разгрузить задачи в формате Task или Task<TResult> в пул потоков, например так:The Task class includes several Run methods that let you easily offload work as a Task or Task<TResult> to the thread pool, for example:

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

Некоторые из этих методов Run, например перегрузка Task.Run(Func<Task>), являются ссылкой на метод TaskFactory.StartNew.Some of these Run methods, such as the Task.Run(Func<Task>) overload, exist as shorthand for the TaskFactory.StartNew method. Другие перегрузки, например Task.Run(Func<Task>), позволяют использовать await в разгруженных задачах, например так:Other overloads, such as Task.Run(Func<Task>), enable you to use await within the offloaded work, for example:

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

Эти перегрузки логически эквивалентны вызову метода TaskFactory.StartNew в сочетании с методом расширения Unwrapиз библиотеки параллельных задач.Such overloads are logically equivalent to using the TaskFactory.StartNew method in conjunction with the Unwrap extension method in the Task Parallel Library.

Task.FromResultTask.FromResult

Используйте метод FromResult в ситуациях, когда данные уже могут быть доступны и их достаточно возвратить в Task<TResult> из метода, возвращающего задачу.Use the FromResult method in scenarios where data may already be available and just needs to be returned from a task-returning method lifted into a Task<TResult>:

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

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

Task.WhenAllTask.WhenAll

Используйте метод WhenAll для асинхронного ожидания нескольких асинхронных операций, которые представлены в виде задач.Use the WhenAll method to asynchronously wait on multiple asynchronous operations that are represented as tasks. У этого метода есть несколько перегрузок, которые поддерживают набор неуниверсальных задач или неоднородный набор универсальных задач (например, асинхронное ожидание нескольких операций, возвращающих void, или асинхронное ожидание несколько методов, возвращающих значение (при этом эти значения могут быть разных типов)), а также поддерживают единый набор универсальных задач (например, асинхронное ожидание нескольких методов, которые возвращают TResult).The method has multiple overloads that support a set of non-generic tasks or a non-uniform set of generic tasks (for example, asynchronously waiting for multiple void-returning operations, or asynchronously waiting for multiple value-returning methods where each value may have a different type) and to support a uniform set of generic tasks (such as asynchronously waiting for multiple TResult-returning methods).

Предположим, что вы хотите отправить сообщения по электронной почте нескольким клиентам.Let's say you want to send email messages to several customers. Отправку сообщений можно перекрывать, чтобы не ожидать завершения отправки одного сообщения перед отправкой следующего.You can overlap sending the messages so you're not waiting for one message to complete before sending the next. Также можно узнать, были ли выполнены операции отправки и возникли ли ошибки.You can also find out when the send operations have completed and whether any errors have occurred:

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

Этот код не обрабатывает возможные исключения явным образом, но позволяет им распространяться из метода await на задачу, полученную от WhenAll.This code doesn't explicitly handle exceptions that may occur, but lets exceptions propagate out of the await on the resulting task from WhenAll. Для обработки исключений можно использовать следующий код.To handle the exceptions, you can use code such as the following:

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

В этом случае при сбое любой асинхронной операции все исключения объединяются в одно исключение AggregateException, которое сохраняется в Task, возвращаемом из метода WhenAll.In this case, if any asynchronous operation fails, all the exceptions will be consolidated in an AggregateException exception, which is stored in the Task that is returned from the WhenAll method. Однако с помощью ключевого слова await распространяется только одно из этих исключений.However, only one of those exceptions is propagated by the await keyword. Если вы хотите изучить все исключения, можно переписать предыдущий код следующим образом.If you want to examine all the exceptions, you can rewrite the previous code as follows:

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

Рассмотрим в качестве примера асинхронную загрузку нескольких файлов из Интернета.Let's consider an example of downloading multiple files from the web asynchronously. В этом случае все асинхронные операции имеют результаты одного типа, и к этим результатам легко получить доступ.In this case, all the asynchronous operations have homogeneous result types, and it's easy to access the results:

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

Можно использовать те же способы обработки исключений, которые были рассмотрены в предыдущем сценарии с возвратом void.You can use the same exception-handling techniques we discussed in the previous void-returning scenario:

Task [] asyncOps =
    (from url in urls select DownloadStringAsync(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.WhenAnyTask.WhenAny

Используйте метод WhenAny для асинхронного ожидания завершения одной из нескольких асинхронных операций, которые представлены в виде задач.You can use the WhenAny method to asynchronously wait for just one of multiple asynchronous operations represented as tasks to complete. Этот метод допускает четыре основных варианта использования.This method serves four primary use cases:

  • Избыточность: многократный запуск одной операции и выбор первой завершенной операции (например, обращение к нескольким веб-сервисам котировок акций с целью получить один результат и выбор операции, которая завершилась первой).Redundancy: Performing an operation multiple times and selecting the one that completes first (for example, contacting multiple stock quote web services that will produce a single result and selecting the one that completes the fastest).

  • Чередование: запуск и ожидание завершения нескольких операций, но обработка операций по мере выполнения.Interleaving: Launching multiple operations and waiting for all of them to complete, but processing them as they complete.

  • Регулирование: добавление новых операций по мере завершения предыдущих.Throttling: Allowing additional operations to begin as others complete. Это расширение сценария с чередованием.This is an extension of the interleaving scenario.

  • Ранняя остановка: например, операция, представленная задачей t1, может сгруппироваться в задачу WhenAny с другой задачей t2, после чего можно ожидать задачу WhenAny.Early bailout: For example, an operation represented by task t1 can be grouped in a WhenAny task with another task t2, and you can wait on the WhenAny task. Например, задача t2 может представлять завершение ожидания, отмену или другой сигнал, требующий завершения задачи WhenAny до завершения задачи t1.Task t2 could represent a time-out, or cancellation, or some other signal that causes the WhenAny task to complete before t1 completes.

ИзбыточностьRedundancy

Рассмотрим случай, когда вам требуется принять решение о необходимости покупки акций.Consider a case where you want to make a decision about whether to buy a stock. Существует несколько стандартных веб-служб с рекомендациями по покупке акций, которым вы доверяете, но в зависимости от ежедневной нагрузки каждая из этих служб иногда может работать медленно.There are several stock recommendation web services that you trust, but depending on daily load, each service can end up being slow at different times. Для получения уведомлений о завершении любой операции можно использовать метод WhenAny:You can use the WhenAny method to receive a notification when any operation completes:

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

В отличие от WhenAll, который возвращает распакованные результаты всех успешно выполненных задач, WhenAny возвращает завершенную задачу.Unlike WhenAll, which returns the unwrapped results of all tasks that completed successfully, WhenAny returns the task that completed. Если задача завершилась сбоем, важно знать, что она завершилась сбоем, а если она завершилась успешно, важно знать, с какой задачей связано возвращаемое значение.If a task fails, it’s important to know that it failed, and if a task succeeds, it’s important to know which task the return value is associated with. Поэтому необходимо получить доступ к результату, возвращаемому задачей, или продолжить ожидание, как показано в данном примере.Therefore, you need to access the result of the returned task, or further await it, as this example shows.

Как и в случае с WhenAll, необходимо поддерживать исключения.As with WhenAll, you have to be able to accommodate exceptions. Так как вы получаете управление от завершенной задачи, вы можете подождать, пока не будут распространены ошибки для возвращенной задачи и try/catch их соответствующим образом.Because you receive the completed task back, you can await the returned task to have errors propagated, and try/catch them appropriately; for example:

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

Кроме того, даже если первая задача завершается успешно, следующие задачи могут завершиться сбоем.Additionally, even if a first task completes successfully, subsequent tasks may fail. В этом случае есть несколько вариантов обработки исключений: можно ждать, пока не завершатся все задачи, используя метод WhenAll, или решить, что все исключения важны и должны быть записаны в журнал.At this point, you have several options for dealing with exceptions: You can wait until all the launched tasks have completed, in which case you can use the WhenAll method, or you can decide that all exceptions are important and must be logged. В этом случае используется продолжение для получения уведомлений об успешном завершении задач.For this, you can use continuations to receive a notification when tasks have completed asynchronously:

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

или даже:or even:

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

Наконец, вы можете отменить все остальные операции.Finally, you may want to cancel all the remaining operations:

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

ЧередованиеInterleaving

Рассмотрим ситуацию, в которой вы загружаете изображения из Интернета и обрабатываете каждое изображение (например, добавляете изображение в элемент управления пользовательского интерфейса).Consider a case where you're downloading images from the web and processing each image (for example, adding the image to a UI control). Обработку изображений необходимо выполнять последовательно в потоке пользовательского интерфейса, но загружать их следует по возможности параллельно.You have to do the processing sequentially on the UI thread, but you want to download the images as concurrently as possible. Кроме того, для добавления изображений в пользовательский интерфейс не стоит ждать, пока все они будут загружены — удобнее добавлять каждое изображение после его загрузки.Also, you don’t want to hold up adding the images to the UI until they’re all downloaded—you want to add them as they complete:

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

Также вы можете применить чередование к сценарию, который подразумевает интенсивную вычислительную обработку пула загруженных изображений ThreadPool, например так:You can also apply interleaving to a scenario that involves computationally intensive processing on the ThreadPool of the downloaded images; for example:

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

РегулированиеThrottling

Рассмотрим пример с чередованием с тем исключением, что на этот раз пользователь загружает так много изображений, что загрузку необходимо регулировать; например, вы можете ограничить максимальное количество параллельных загрузок.Consider the interleaving example, except that the user is downloading so many images that the downloads have to be throttled; for example, you want only a specific number of downloads to happen concurrently. Для этого можно запустить подмножество асинхронных операций.To achieve this, you can start a subset of the asynchronous operations. По завершении операций можно запускать дополнительные операции, которые займут их место.As operations complete, you can start additional operations to take their place:

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

Ранняя остановкаEarly Bailout

Рассмотрим, что вы асинхронно ожидаете завершения операции и одновременно отвечаете на запрос отмены пользователя (например, если пользователь нажал кнопку "Отмена").Consider that you're waiting asynchronously for an operation to complete while simultaneously responding to a user’s cancellation request (for example, the user clicked a cancel button). Этот сценарий иллюстрируется в следующем коде.The following code illustrates this scenario:

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

В этой реализации сразу же после отмены загрузки отображается пользовательский интерфейс, но базовые асинхронные операции не отменяются.This implementation re-enables the user interface as soon as you decide to bail out, but doesn't cancel the underlying asynchronous operations. В качестве альтернативы можно отменить ожидающие операции после отмены загрузки, но не отображать пользовательский интерфейс, пока операции на самом деле не завершатся (возможно, из-за раннего завершения, вызванного запросом отмены).Another alternative would be to cancel the pending operations when you decide to bail out, but not reestablish the user interface until the operations actually complete, potentially due to ending early due to the cancellation request:

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

Еще один пример ранней остановки предполагает использование метода WhenAny в сочетании с методом Delay, как описано в следующем разделе.Another example of early bailout involves using the WhenAny method in conjunction with the Delay method, as discussed in the next section.

Task.DelayTask.Delay

Метод Task.Delay позволяет приостановить выполнение асинхронного метода.You can use the Task.Delay method to introduce pauses into an asynchronous method’s execution. Это удобно для реализации различных функций, включая создание циклов опроса и задержку обработки ввода пользователя на заданный период времени.This is useful for many kinds of functionality, including building polling loops and delaying the handling of user input for a predetermined period of time. Метод Task.Delay также можно использовать в сочетании с Task.WhenAny для ограничения времени ожидания await.The Task.Delay method can also be useful in combination with Task.WhenAny for implementing time-outs on awaits.

Если на выполнение задачи, которая является частью большой асинхронной операции (например, веб-служба ASP.NET), требуется слишком много времени, то это может негативно сказаться на всей операции, особенно если это приведет к неудачному завершению операции.If a task that’s part of a larger asynchronous operation (for example, an ASP.NET web service) takes too long to complete, the overall operation could suffer, especially if it fails to ever complete. Поэтому важно иметь возможность задавать время ожидания для асинхронных операций.For this reason, it’s important to be able to time out when waiting on an asynchronous operation. Синхронные методы Task.Wait, Task.WaitAll и Task.WaitAny принимают значения времени ожидания, а соответствующий метод TaskFactory.ContinueWhenAll/Task.WhenAny и ранее упомянутый Task.WhenAll/Task.WhenAny — нет.The synchronous Task.Wait, Task.WaitAll, and Task.WaitAny methods accept time-out values, but the corresponding TaskFactory.ContinueWhenAll/Task.WhenAny and the previously mentioned Task.WhenAll/Task.WhenAny methods do not. Вместо этого вы можете совместно использовать Task.Delay и Task.WhenAny для ограничения времени ожидания.Instead, you can use Task.Delay and Task.WhenAny in combination to implement a time-out.

Например, предположим, что вы хотите загрузить приложение и отключить пользовательский интерфейс на время загрузки.For example, in your UI application, let's say that you want to download an image and disable the UI while the image is downloading. Однако если загрузка занимает слишком много времени, вы можете отменить загрузку и вернуться в пользовательский интерфейс.However, if the download takes too long, you want to re-enable the UI and discard the download:

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

Это же применимо и скачиванию нескольких файлов, так как WhenAll возвращает задачу:The same applies to multiple downloads, because WhenAll returns a task:

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) panel.AddImage(bmp);
            status.Text = "Downloaded";
        }
        else
        {
            status.Text = "Timed out";
            downloads.ContinueWith(t => Log(t));
        }
    }
    finally { btnDownload.Enabled = true; }
}

Создание блоков объединения на основе задачBuilding Task-based Combinators

Поскольку задача может полностью представлять асинхронную операцию и предоставляет синхронные и асинхронные функции для соединения с операцией, получения ее результатов и т. д., то вы можете создавать полезные библиотеки блоков объединения, которые объединяют задачи для создания шаблонов большего размера.Because a task is able to completely represent an asynchronous operation and provide synchronous and asynchronous capabilities for joining with the operation, retrieving its results, and so on, you can build useful libraries of combinators that compose tasks to build larger patterns. Как уже обсуждалось в предыдущем разделе, в платформе .NET Framework есть несколько встроенных блоков объединения, но вы можете создать и собственные.As discussed in the previous section, the .NET Framework includes several built-in combinators, but you can also build your own. В следующих разделах приведено несколько примеров возможных методов и типов блоков объединения.The following sections provide several examples of potential combinator methods and types.

RetryOnFaultRetryOnFault

Во многих ситуациях может потребоваться повторить операцию, если предыдущая попытка завершилась неудачно.In many situations, you may want to retry an operation if a previous attempt fails. Для решения этой задачи в синхронном коде можно использовать вспомогательный метод, например RetryOnFault в следующем примере.For synchronous code, you might build a helper method such as RetryOnFault in the following example to accomplish this:

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

Вы можете создать почти такой же вспомогательный метод для асинхронных операций, которые реализованы в TAP и таким образом вернуть задачи.You can build an almost identical helper method for asynchronous operations that are implemented with TAP and thus return tasks:

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

Этот блок объединения также можно использовать для анализа повторных попыток в логике приложения.You can then use this combinator to encode retries into the application’s logic; for example:

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

Функцию RetryOnFault можно улучшить.You could extend the RetryOnFault function further. Например, функция может принимать другой Func<Task>, который будет вызываться между повторными попытками, чтобы определить, когда операцию нужно повторить.For example, the function could accept another Func<Task> that will be invoked between retries to determine when to try the operation again; for example:

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

Чтобы подождать одну секунду перед повтором операции, эту функцию можно использовать следующим образом.You could then use the function as follows to wait for a second before retrying the operation:

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

NeedOnlyOneNeedOnlyOne

В некоторых случаях для повышения задержки и вероятности успешного завершения операции можно воспользоваться преимуществами избыточности.Sometimes, you can take advantage of redundancy to improve an operation’s latency and chances for success. Рассмотрим несколько веб-служб, которые предоставляют котировки акций в разное время дня, и каждая служба обеспечивает различный уровень качества и время отклика.Consider multiple web services that provide stock quotes, but at various times of the day, each service may provide different levels of quality and response times. Чтобы справиться с неравномерным характером поступления данных, вы можете отправлять запросы ко всем веб-службам и при получении ответа от одной из веб-служб отменять оставшиеся запросы.To deal with these fluctuations, you may issue requests to all the web services, and as soon as you get a response from one, cancel the remaining requests. Вы можете реализовать вспомогательную функцию для более удобной реализации этого распространенного шаблона с запуском нескольких операций, ожидания завершения любой операции и последующей отмены остальных.You can implement a helper function to make it easier to implement this common pattern of launching multiple operations, waiting for any, and then canceling the rest. Функция NeedOnlyOne в следующем примере иллюстрирует этот сценарий.The NeedOnlyOne function in the following example illustrates this scenario:

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

Затем можно использовать эту функцию следующим образом.You can then use this function as follows:

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

Операции с чередованиемInterleaved Operations

При использовании метода WhenAny для поддержки сценария чередования при работе с очень большими наборами задач существует потенциальная проблема производительности.There is a potential performance problem with using the WhenAny method to support an interleaving scenario when you're working with very large sets of tasks. Каждый вызов WhenAny приводит к регистрации продолжения в каждой задаче.Every call to WhenAny results in a continuation being registered with each task. Для N задач это приводит к созданию O(N2) продолжений в течение времени существования операции чередования.For N number of tasks, this results in O(N2) continuations created over the lifetime of the interleaving operation. При работе с большим набором задач можно использовать комбинатор (Interleaved в следующем примере), чтобы решить проблему производительности.If you're working with a large set of tasks, you can use a combinator (Interleaved in the following example) to address the performance issue:

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

Затем с помощью блоков объединения можно объединять результаты задач по мере их завершения.You can then use the combinator to process the results of tasks as they complete; for example:

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

WhenAllOrFirstExceptionWhenAllOrFirstException

В некоторых сценариях распределения и объединения вы можете ожидать, пока одна из задач в наборе не завершится сбоем, в этом случае ожидание прекращается, так как выдается исключение.In certain scatter/gather scenarios, you might want to wait for all tasks in a set, unless one of them faults, in which case you want to stop waiting as soon as the exception occurs. Для этого можно использовать такой блок объединения как WhenAllOrFirstException, как в следующем примере.You can accomplish that with a combinator method such as WhenAllOrFirstException in the following example:

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

Создание структур данных на основе задачBuilding Task-based Data Structures

Кроме возможности создавать блоки объединения на основе задач, Task и Task<TResult> имеют структуру данных, которая представляет результаты асинхронной операции и необходимую синхронизацию для объединения, что делает их очень мощным инструментом для создания пользовательских структур данных для асинхронных сценариев.In addition to the ability to build custom task-based combinators, having a data structure in Task and Task<TResult> that represents both the results of an asynchronous operation and the necessary synchronization to join with it makes it a very powerful type on which to build custom data structures to be used in asynchronous scenarios.

AsyncCacheAsyncCache

Одним из важных аспектов задачи является то, что ее можно передать нескольким потребителям, каждый из которых может ожидать ее, регистрировать продолжения для этой задачи, получать ее результат или исключения (для Task<TResult>) и т. д.One important aspect of a task is that it may be handed out to multiple consumers, all of whom may await it, register continuations with it, get its result or exceptions (in the case of Task<TResult>), and so on. Благодаря этому Task и Task<TResult> идеально подходят для асинхронной инфраструктуры кэширования.This makes Task and Task<TResult> perfectly suited to be used in an asynchronous caching infrastructure. Ниже приведен пример небольшого, но мощного асинхронного кэша, созданного на основе Task<TResult>:Here’s an example of a small but powerful asynchronous cache built on top of Task<TResult>:

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("loader");
        _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;
        }
    }
}

Класс AsyncCache<TKey,TValue в качестве делегата своего конструктора принимает функцию, которая принимает значение TKey и возвращает значение Task<TResult>.The AsyncCache<TKey,TValue> class accepts as a delegate to its constructor a function that takes a TKey and returns a Task<TResult>. Ранее запрошенные из кэша значения хранятся во внутреннем словаре, и AsyncCache гарантирует, что для одного ключа создается только одна задача, даже при одновременном доступе к кэшу.Any previously accessed values from the cache are stored in the internal dictionary, and the AsyncCache ensures that only one task is generated per key, even if the cache is accessed concurrently.

Например, можно создать кэш для загруженных веб-страниц.For example, you can build a cache for downloaded web pages:

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

Затем можно использовать этот кэш в асинхронных методах каждый раз, когда вам потребуется содержимое какой-либо веб-страницы.You can then use this cache in asynchronous methods whenever you need the contents of a web page. Класс AsyncCache гарантирует, что будет загружено минимальное число страниц, и кэширует результаты.The AsyncCache class ensures that you’re downloading as few pages as possible, and caches the results.

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

AsyncProducerConsumerCollectionAsyncProducerConsumerCollection

Задачи также можно использовать для создания структур данных для координации асинхронных действий.You can also use tasks to build data structures for coordinating asynchronous activities. Рассмотрим один из классических шаблонов параллельных систем: производитель/потребитель.Consider one of the classic parallel design patterns: producer/consumer. В этой схеме производители создают данные, которые используются потребителями и производители и потребители могут работать параллельно.In this pattern, producers generate data that is consumed by consumers, and the producers and consumers may run in parallel. Например, потребитель обрабатывает элемент 1, который ранее был создан производителем, который в это же время создает элемент 2.For example, the consumer processes item 1, which was previously generated by a producer who is now producing item 2. Для схемы "производитель/потребитель" в любом случае потребуется некоторая структура данных, в которой будут храниться объекты, создаваемые производителем. Эта структура необходима для того, чтобы потребитель мог узнать о новых данных и получить их, когда они будут доступны.For the producer/consumer pattern, you invariably need some data structure to store the work created by producers so that the consumers may be notified of new data and find it when available.

Вот простая структура данных на основе задач, которая позволяет использовать асинхронные методы в качестве производителей и потребителей.Here’s a simple data structure built on top of tasks that enables asynchronous methods to be used as producers and consumers:

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

С этой структурой данных на месте можно написать следующий код.With that data structure in place, you can write code such as the following:

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 включает также тип BufferBlock<T>, который можно использовать аналогичным образом, но не создавая пользовательский тип коллекции:The System.Threading.Tasks.Dataflow namespace includes the BufferBlock<T> type, which you can use in a similar manner, but without having to build a custom collection type:

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

Примечание

Пространство имен System.Threading.Tasks.Dataflow доступно в .NET Framework 4.5 через NuGet.The System.Threading.Tasks.Dataflow namespace is available in the .NET Framework 4.5 through NuGet. Чтобы установить сборку, которая содержит пространство имен System.Threading.Tasks.Dataflow, откройте проект в Visual Studio, в меню "Проект" выберите пункт Управление пакетами NuGet и найдите в Интернете пакет Microsoft.Tpl.Dataflow.To install the assembly that contains the System.Threading.Tasks.Dataflow namespace, open your project in Visual Studio, choose Manage NuGet Packages from the Project menu, and search online for the Microsoft.Tpl.Dataflow package.

См. такжеSee also