2 Task Parallelism 任务并行性

In the previous chapter, we introduced the concept of parallel programming. In this
chapter, we will move on to discussing TPL and task parallelism.
One of the major goals of .NET as a programming framework is to make a developer's life
easier by wrapping up all the commonly required tasks as APIs. As we have already seen,
threads have existed since the earliest versions of .NET, but they were initially very
complex and were associated with a lot of overhead. Microsoft has introduced a lot of new
parallel primitives that make it easier to write, debug, and maintain parallel programs from
scratch, without having to deal with the complexities that are involved with legacy
threading.
The following topics will be covered in this chapter:
Creating and starting a task
Getting results from finished tasks
How to cancel tasks
How to wait on running tasks
Handling task exceptions
Converting Asynchronous Programming Model (APM) patterns into tasks
Converting Event-Based Asynchronous Patterns (EAPs) into tasks
More on tasks:
Continuation tasks
Parent and child tasks
Local and global queues and storage
Work-stealing queues

在前一章中,我们介绍了并行编程的概念。在这一章中,我们将继续讨论TPL(任务并行库)和任务并行性。

.NET作为一个编程框架的主要目标之一是通过将常见任务封装为API来简化开发人员的工作。正如我们已经看到的,线程自.NET的最早版本以来就已经存在了,但它们最初非常复杂,并且伴随着大量的开销。微软引入了许多新的并行原语,使得从零开始编写、调试和维护并行程序变得更加容易,而无需处理与遗留线程相关的复杂性。

以下主题将在本章中涵盖:
- 创建和启动任务
- 从已完成的任务获取结果
- 如何取消任务
- 如何等待正在运行的任务
- 处理任务异常
- 将异步编程模型(APM)模式转换为任务
- 将基于事件的异步模式(EAP)转换为任务
- 更多关于任务的内容:
  - 连续任务
  - 父子任务
  - 本地和全局队列和存储
  - 工作窃取队列

Technical requirements
To complete this chapter, you should have a good understanding of C# and some advanced
concepts such as delegates.
The source code for this chapter is available on GitHub at https: /​/​github. ​com/
PacktPublishing/​Hands-​On-​Parallel-​Programming-​with-​C-​8-​and-​. ​NET-​Core-​3/​tree/
master/​Chapter02 .

技术要求
要完成本章,您应该对C#和一些高级概念(如委托)有深入的理解。
本章的源代码可在GitHub上获得,网址为:https://​github.​com/​PacktPublishing/​Hands-​On-​Parallel-​Programming-​with-​C-​8-​and-​.​NET-​Core-​3/​tree/​master/​Chapter02。

Tasks
Tasks are abstractions in .NET that provide units of asynchrony, just like promises in
JavaScript. In initial versions of .NET, we had to rely on threads only, which were created
either directly or using the ThreadPool class. The ThreadPool class provided a managed
abstraction layer over threads but developers still relied on the Thread class for better
control. By creating a thread via the Thread class, we gained access to the underlying
object, which we can wait for, cancel, or move to the foreground or background. In real
time, however, we required threads to perform work continuously. This required us to
write lots of code, which was difficult to maintain. The Thread class was also unmanaged,
which put a high burden on both the memory and the CPU. We needed the best of both
worlds, which is where tasks come to the rescue. A task is nothing but a wrapper over a
thread, which is created via ThreadPool . Tasks provide features such as await,
cancellation, and continuation, and these run after a task has finished.
Tasks have the following important features:
Tasks are executed by a TaskScheduler and the default scheduler simply runs
on ThreadPool .
We can return values from tasks.
Tasks let you know when they finish, unlike ThreadPool or threads.
A task can be run in continuation using the ContinueWith() construct.
We can wait on tasks by calling Task. Wait() . This blocks the calling thread
until it has finished.
Tasks make the code much more readable compared to legacy threads or
ThreadPool . They also paved the way to the introduction of the asynchronous
programming construct in C# 5.0.
We can establish a parent/child relationship when one task is started from
another task.
We can propagate child task exceptions to parent tasks.
A task can be canceled using the CancellationToken class.

任务
任务是.NET中的一种抽象,提供了异步单元,就像JavaScript中的承诺一样。在.NET的最初版本中,我们只能依赖线程,这些线程可以直接创建或使用ThreadPool类创建。ThreadPool类提供了一个对线程的管理抽象层,但开发人员仍然依赖Thread类来获得更好的控制。通过Thread类创建一个线程,我们可以访问底层对象,可以等待、取消或将其移动到前台或后台。然而,在实时中,我们需要线程持续执行工作。这要求我们编写大量难以维护的代码。Thread类也是未管理的,这对内存和CPU都造成了很高的负担。我们需要两者兼得的最佳方案,这就是任务发挥作用的地方。任务只是一个通过ThreadPool创建的线程的包装器。任务提供了诸如await、取消和延续等功能,这些功能在一个任务完成后运行。
任务具有以下重要特性:
任务由TaskScheduler执行,默认调度程序简单地在ThreadPool上运行。
我们可以从任务中返回值。
任务让你知道它们何时完成,不像ThreadPool或线程那样。
可以使用ContinueWith()构造继续运行任务。
我们可以通过调用Task.Wait()等待任务完成。这将阻塞调用线程,直到它完成。
与遗留的线程或ThreadPool相比,任务使代码更易读。它们还为C# 5.0中引入的异步编程构造铺平了道路。
当一个任务从另一个任务启动时,我们可以建立父子关系。
我们可以将子任务异常传播到父任务。
可以使用CancellationToken类取消任务。

Creating and starting a task
There are many ways in which we can create and run a task using the TPL. In this section,
we will try to understand all of these approaches and do a comparative analysis wherever
we can. First, you need to add a reference to the System. Threading. Tasks namespace:
using System. Threading. Tasks;
We will try to create a task using the following approaches:
The System. Threading. Tasks. Task class
The System. Threading. Tasks. Task. Factory. StartNew method
The System. Threading. Tasks. Task. Run method
System. Threading. Tasks. Task. Delay
System. Threading. Tasks. Task. Yield
System. Threading. Tasks. Task. FromResult<T> Method
System. Threading. Tasks. Task. FromException and
Task. FromException<T>
System. Threading. Tasks. Task. FromCancelled and
Task. FromCancelled<T>

创建和启动任务
我们可以通过多种方式使用TPL(任务并行库)创建和运行任务。在这一部分中,我们将尝试理解所有这些方法,并在可能的情况下进行比较分析。首先,你需要添加对System.Threading.Tasks命名空间的引用:
```csharp
using System.Threading.Tasks;
```
我们将尝试使用以下方法创建一个任务:
- System.Threading.Tasks.Task类
- System.Threading.Tasks.Task.Factory.StartNew方法
- System.Threading.Tasks.Task.Run方法
- System.Threading.Tasks.Task.Delay
- System.Threading.Tasks.Task.Yield
- System.Threading.Tasks.Task.FromResult<T>方法
- System.Threading.Tasks.Task.FromException和Task.FromException<T>
- System.Threading.Tasks.Task.FromCancelled和Task.FromCancelled<T>

The System.Threading.Tasks.Task class
A task class is a way of executing work asynchronously as a ThreadPool thread and is
based on the Task-Based Asynchronous Pattern (TAP). The non-generic Task class doesn't
return results, so whenever we need to return values from a task, we need to use the
generic version, Task<T> . The tasks that are created via the Task class are not scheduled to
run until we call the Start method.
We can create a task using the Task class in various ways, all of which we'll cover in the
following subsections.

System.Threading.Tasks.Task类
Task类是一种作为ThreadPool线程异步执行工作的方式,它基于任务型异步模式(TAP)。非泛型的Task类不返回结果,所以每当我们需要从任务中返回值时,我们需要使用泛型版本Task<T>。通过Task类创建的任务在我们调用Start方法之前不会被安排运行。
我们可以使用Task类以多种方式创建任务,所有这些方式我们都将在下面的小节中介绍。

Using lambda expressions syntax
In the following code, we are creating a task by calling the Task constructor and passing a
lambda expression containing the method we want to execute:
Task task = new Task (() => PrintNumber10Times () ) ;
task. Start() ;
Using the Action delegate
In the following code, we are creating a task by calling the Task constructor and passing a
delegate containing the method we want to execute:
Task task = new Task (new Action (PrintNumber10Times) ) ;
task. Start() ;
Using delegate
In the following code, we are creating a task object by calling the Task constructor and
passing an anonymous delegate containing the method we want to execute:
Task task = new Task (delegate {PrintNumber10Times () ; }) ;
task. Start() ;
In all of these cases, the output will be as follows:
All the preceding methods do the same thing – they just have different syntaxes.
We can only call the Start method on tasks that have not run previously.
If you need to rerun a task that has already been completed, you need to
create a new task and call the Start method on that.

使用lambda表达式语法
在下面的代码中,我们通过调用Task构造函数并传递一个包含要执行的方法的lambda表达式来创建一个任务:
```csharp
Task task = new Task(() => PrintNumber10Times());
task.Start();
```

使用Action委托
在下面的代码中,我们通过调用Task构造函数并传递一个包含要执行的方法的委托来创建一个任务:
```csharp
Task task = new Task(new Action(PrintNumber10Times));
task.Start();
```

使用委托
在下面的代码中,我们通过调用Task构造函数并传递一个包含要执行的方法的匿名委托来创建一个任务对象:
```csharp
Task task = new Task(delegate { PrintNumber10Times(); });
task.Start();
```

在所有这些情况下,输出将如下所示:
所有前面的方法都做相同的事情——它们只是具有不同的语法。
我们只能在尚未运行过的任务上调用Start方法。
如果您需要重新运行已完成的任务,您需要创建一个新的任务并在其上调用Start方法。

The
System.Threading.Tasks.Task.Factory.StartNew
method
We can also create a task using the StartNew method of the TaskFactory class, as follows.
In this approach, the task is created and scheduled for execution inside the ThreadPool
and a reference of that Task is returned to the caller.
We can create a task using the Task. Factory. StartNew method. We'll go over this in the
following subsections.
Using lambda expressions syntax
In the following code, we are creating a Task by calling the StartNew() method on
TaskFactory and passing a lambda expression containing the method we want to execute:
Task. Factory. StartNew(() => PrintNumber10Times() ) ;
Using the Action delegate
In the following code, we are creating a Task by calling the StartNew() method on
TaskFactory and passing a delegate wrapping method that we want to execute:
Task. Factory. StartNew(new Action( PrintNumber10Times) ) ;
Using delegate
In the following code, we are creating a Task by calling the StartNew() method on
TaskFactory and passing the delegate wrapping method we want to execute:
Task. Factory. StartNew(delegate { PrintNumber10Times() ; }) ;
All the preceding methods do the same thing – they just have different syntaxes.

System.Threading.Tasks.Task.Factory.StartNew方法
我们还可以使用TaskFactory类的StartNew方法来创建一个任务,如下所示。
在这种方法中,任务被创建并安排在ThreadPool中执行,并且该任务的引用返回给调用者。
我们可以使用Task.Factory.StartNew方法来创建一个任务。我们将在接下来的小节中讨论这个。
使用lambda表达式语法
在下面的代码中,我们通过在TaskFactory上调用StartNew()方法并传递一个包含我们想要执行的方法的lambda表达式来创建一个Task:
```csharp
Task.Factory.StartNew(() => PrintNumber10Times());
```

使用Action委托
在下面的代码中,我们通过在TaskFactory上调用StartNew()方法并传递一个包装我们想要执行的方法的委托来创建一个Task:
```csharp
Task.Factory.StartNew(new Action(PrintNumber10Times));
```

使用委托
在下面的代码中,我们通过在TaskFactory上调用StartNew()方法并传递一个包装我们想要执行的方法的委托来创建一个Task:
```csharp
Task.Factory.StartNew(delegate { PrintNumber10Times(); });
```
所有前面的方法都做相同的事情——它们只是具有不同的语法。

The System.Threading.Tasks.Task.Run method
We can also create a task using the Task. Run method. This works just like the StartNew
method and returns a ThreadPool thread.
We can create a Task using the Task. Run method in the following ways, all of which will
be discussed in the following subsections.
Using lambda expressions syntax
In the following code, we are creating a Task by calling the static Run() method on Task
and passing a lambda expression containing the method we want to execute:
Task. Run(() => PrintNumber10Times () ) ;
Using the Action delegate
In the following code, we are creating a Task by calling the static Run() method on Task
and passing a delegate containing the method we want to execute:
Task. Run(new Action (PrintNumber10Times) ) ;
Using delegate
In the following code, we are creating a Task by calling the static Run() method on Task
and passing a delegate containing the method we want to execute:
Task. Run(delegate {PrintNumber10Times () ; }) ;

System.Threading.Tasks.Task.Run方法
我们还可以使用Task.Run方法来创建一个任务。这就像StartNew方法一样,并返回一个ThreadPool线程。
我们可以使用以下方式使用Task.Run方法创建任务,所有这些都将在接下来的小节中讨论。
使用lambda表达式语法
在下面的代码中,我们通过调用Task上的静态Run()方法并传递包含我们要执行的方法的lambda表达式来创建一个任务:
```csharp
Task.Run(() => PrintNumber10Times());
```

使用Action委托
在下面的代码中,我们通过调用Task上的静态Run()方法并传递包含我们要执行的方法的委托来创建一个任务:
```csharp
Task.Run(new Action(PrintNumber10Times));
```

使用委托
在下面的代码中,我们通过调用Task上的静态Run()方法并传递包含我们要执行的方法的委托来创建一个任务:
```csharp
Task.Run(delegate {PrintNumber10Times(); });
```

The System.Threading.Tasks.Task.Delay method
We can create a task that completes after a specified interval of time or that can be canceled
at any time by the user using the CancellationToken class. In the past, we used
the Thread. Sleep() method of the Thread class to create blocking constructs to wait on
other tasks. The problem with this approach, however, was that it still used CPU resources
and ran synchronously. Task. Delay provides a better alternative to waiting on tasks
without utilizing CPU cycles. It also runs asynchronously:
Console. WriteLine("What is the output of 20/2. We will show result in 2
seconds. ") ;
Task. Delay(2000) ;
Console. WriteLine("After 2 seconds delay") ;
Console. WriteLine("The output is 10") ;
The preceding code asks the user a question and then waits for two seconds before
presenting the answer. During those two seconds, the main thread doesn't have to wait but
has to carry out other tasks to improve the user's experience. The code runs asynchronously
on the system clock and, once the time expires, the rest of the code is executed.
The output of the preceding code is as follows:
Before looking at the other methods we can use to create a task, we'll take a look at two
asynchronous programming constructs that were introduced in C# 5.0: the async and
await keywords.
async and await are code markers that make it easier for us to write asynchronous
programs. We will learn about these keywords in depth in Chapter 9 , Async, Await, and
Task-Based Asynchronous Programming Basics. As the name suggests, we can wait on any
asynchronous call using the await keyword. The moment the executing thread encounters
the await keyword inside a method, it returns to ThreadPool , marks the rest of the
method as a continuation delegate, and starts executing the other queued tasks. Once the
asynchronous task finishes, any available thread from ThreadPool finishes the rest of the
method. 

System.Threading.Tasks.Task.Delay方法
我们可以创建一个在指定时间间隔后完成的任务,或者使用CancellationToken类由用户随时取消的任务。过去,我们使用Thread类的Thread.Sleep()方法来创建阻塞结构以等待其他任务。然而,这种方法的问题是它仍然使用了CPU资源,并且同步运行。Task.Delay提供了一个更好的替代方案来等待任务,而不占用CPU周期。它也异步运行:
```csharp
Console.WriteLine("20除以2的结果是多少。我们将在2秒后显示结果。");
Task.Delay(2000);
Console.WriteLine("经过2秒的延迟");
Console.WriteLine("结果是10");
```
上面的代码向用户提出一个问题,然后等待两秒钟再呈现答案。在那两秒钟内,主线程不必等待,但必须执行其他任务以提高用户体验。代码在系统时钟上异步运行,一旦时间到期,就执行其余的代码。
上述代码的输出如下:


在查看我们可以用来创建任务的其他方法之前,我们将了解C# 5.0中引入的两个异步编程构造:async和await关键字。
async和await是使我们更容易编写异步程序的代码标记。我们将在第9章深入学习这些关键字,即“Async, Await以及基于任务的异步编程基础”。顾名思义,我们可以使用await关键字等待任何异步调用。执行线程遇到方法内的await关键字时,它返回到ThreadPool,将方法的其余部分标记为延续委托,并开始执行其他排队的任务。一旦异步任务完成,ThreadPool中的任何可用线程完成方法的其余部分。

The System.Threading.Tasks.Task.Yield method
This is another way of creating an await task. The underlying task is not directly accessible
to the caller but is used in some scenarios involving asynchronous programming that are
related to program execution. It is more like a promise than a task. Using Task. Yield , we
can force our method to be asynchronous and return control to the OS. When the rest of the
method executes at a later point in time, it may still run as asynchronous code. We can
achieve the same effect using the following code:
await Task. Factory. StartNew(() => {},
CancellationToken. None,
TaskCreationOptions. None,
SynchronizationContext. Current ! = null?
TaskScheduler. FromCurrentSynchronizationContext() :
TaskScheduler. Current) ;
This approach can be used to make UI applications responsive by providing control to the
UI thread from time to time inside long-running tasks. However, this is not the preferred
approach for UI applications. There are better alternatives, which are available in the form
of Application. DoEvents() in WinForms and Dispatcher. Yield
(DispatcherPriority. ApplicationIdle) in WPF:
private async static void TaskYield()
{
for (int i = 0; i < 100000; i++)
{
Console. WriteLine(i) ;
if (i % 1000 == 0)
await Task. Yield() ;
}
}
In the case of console or web applications, when we run the code and apply a breakpoint on
the task's yield, we will see random thread pool threads switching context to run the code.
The following screenshots depict various threads controlling execution at various stages.

The following screenshot shows all the threads executing at the same time in the program
flow. We can see that the current thread ID is 1664:

If we press F5 and allow the breakpoint to get hit for another value of i , we will see that the
code is now being executed by another thread with an ID of 10244:
We will learn more about thread windows and debugging techniques in Chapter 11 ,
Writing Unit Test Cases for Parallel and Asynchronous Code.

System.Threading.Tasks.Task.Yield方法
这是创建等待任务的另一种方式。底层任务对调用者不直接可见,但在涉及异步编程的某些场景中使用,与程序执行相关。它更像是一个承诺而不是一个任务。使用Task.Yield,我们可以强制我们的方法异步执行并将控制权返回给操作系统。当方法的其余部分在稍后的时间点执行时,它可能仍然作为异步代码运行。我们可以使用以下代码实现相同的效果:
```csharp
await Task.Factory.StartNew(() => {},
CancellationToken.None,
TaskCreationOptions.None,
SynchronizationContext.Current != null ?
TaskScheduler.FromCurrentSynchronizationContext() :
TaskScheduler.Current);
```
这种方法可以用于通过在长时间运行的任务中不时将控制权提供给UI线程,使UI应用程序保持响应性。然而,这不是UI应用程序的首选方法。有更好替代方案,如WinForms中的Application.DoEvents()和WPF中的Dispatcher.Yield(DispatcherPriority.ApplicationIdle):
```csharp
private async static void TaskYield()
{
for (int i = 0; i < 100000; i++)
{
Console.WriteLine(i);
if (i % 1000 == 0)
await Task.Yield();
}
}
```
在控制台或Web应用程序的情况下,当我们运行代码并在任务的yield上设置断点时,我们将看到随机的线程池线程切换上下文来运行代码。下面的截图显示了各种线程在不同阶段控制执行。

下面的截图显示了程序流中同时执行的所有线程。我们可以看到当前线程ID是1664:

如果我们按F5并允许断点为另一个i值命中,我们将看到代码现在由另一个线程执行,其ID为10244:


我们将在第11章“为并行和异步代码编写单元测试用例”中了解更多关于线程窗口和调试技术的信息。

The
System.Threading.Tasks.Task.FromResult<T>
method
This approach, which was introduced recently in .NET framework 4.5, is very much
underrated. We can return a completed task with results via this approach, as shown here:
static void Main(string[] args)
{
StaticTaskFromResultUsingLambda() ;
}
private static void StaticTaskFromResultUsingLambda()
{
Task<int> resultTask = Task. FromResult<int>( Sum(10) ) ;
Console. WriteLine(resultTask. Result) ;
}
private static int Sum (int n)
{
int sum=0;
for (int i = 0; i < 10; i++)
{
sum += i;
}
return sum;
}
As you can see from the preceding code, we have actually converted a synchronous Sum
method to return results in an asynchronous manner using the Task. FromResult<int>
class. This approach is frequently used in TDD for mocking asynchronous methods, as well
as inside asynchronous methods to return default values based on conditions. We will
explain these approaches in Chapter 11 , Writing Unit Test Cases for Parallel and
Asynchronous Code.

System.Threading.Tasks.Task.FromResult<T>方法
这种方法是在.NET框架4.5中最近引入的,非常被低估。通过这种方法,我们可以返回一个已完成的任务和结果,如下所示:
```csharp
static void Main(string[] args)
{
StaticTaskFromResultUsingLambda();
}
private static void StaticTaskFromResultUsingLambda()
{
Task<int> resultTask = Task.FromResult<int>(Sum(10));
Console.WriteLine(resultTask.Result);
}
private static int Sum(int n)
{
int sum=0;
for (int i = 0; i < 10; i++)
{
sum += i;
}
return sum;
}
```
从前面的代码可以看出,我们实际上已经将同步的Sum方法转换为以异步方式返回结果,使用了Task.FromResult<int>类。这种方法在TDD中用于模拟异步方法,以及在异步方法内部根据条件返回默认值时经常使用。我们将在第11章“为并行和异步代码编写单元测试用例”中解释这些方法。

The
System.Threading.Tasks.Task.FromException
and
System.Threading.Tasks.Task.FromException<T>
methods
These methods create tasks that have completed with a predefined exception and are used
to throw exceptions from asynchronous tasks, as well as in TDD. We will explain this
approach further in Chapter 11 , Writing Unit Test Cases for Parallel and Asynchronous Code:
return Task. FromException<long>(
new FileNotFoundException("Invalid File name. ") ) ;
As you can see in the preceding code, we are wrapping FileNotFoundException as a task
and returning it to the caller.

System.Threading.Tasks.Task.FromException 和 System.Threading.Tasks.Task.FromException<T> 方法
这些方法创建了已完成并带有预定义异常的任务,用于从异步任务抛出异常,以及在TDD中使用。我们将在第11章“为并行和异步代码编写单元测试用例”中进一步解释这种方法:
```csharp
return Task.FromException<long>(new FileNotFoundException("Invalid File name."));
```
如前所述的代码所示,我们将FileNotFoundException包装成一个任务并返回给调用者。

The System.Threading.Tasks.Task.FromCanceled
and
System.Threading.Tasks.Task.FromCanceled<T>
methods
These methods are used to create tasks that have completed as a result of cancellation via
the cancellation token:
CancellationTokenSource source = new CancellationTokenSource() ;
var token = source. Token;
source. Cancel() ;
Task task = Task. FromCanceled(token) ;
Task<int> canceledTask = Task. FromCanceled<int>(token) ;
As shown in the preceding code, we created a cancellation token using
the CancellationTokenSource class. Then, we created a task from that token. The
important thing to consider here is that the token needs to be canceled before we can use it
with the Task. FromCanceled method.
This approach is useful if we want to return values from asynchronous methods, as well as
in TDD.

System.Threading.Tasks.Task.FromCanceled 和 System.Threading.Tasks.Task.FromCanceled<T> 方法
这些方法用于创建因取消令牌而完成的任务:
```csharp
CancellationTokenSource source = new CancellationTokenSource();
var token = source.Token;
source.Cancel();
Task task = Task.FromCanceled(token);
Task<int> canceledTask = Task.FromCanceled<int>(token);
```
如前所述的代码所示,我们使用CancellationTokenSource类创建了一个取消令牌。然后,我们根据该令牌创建了一个任务。这里需要考虑的重要事项是,在使用Task.FromCanceled方法之前,令牌需要被取消。
这种方法在我们想要从异步方法返回值,以及在TDD中很有用。

Getting results from finished tasks
To return values from tasks, TPL provides a generic variant of all of the classes that we
defined previously:
Task<T>
Task. Factory. StartNew<T>
Task. Run<T>
Once a task finishes, we should be able to get results from it by accessing
the Task. Result property. Let's try to understand this using some code examples. We will
create various tasks and try to return values from them on completion:
using System;
using System. Threading. Tasks;
namespace Ch02
{
class _2GettingResultFromTasks
{
static void Main(string[] args)
{
GetResultsFromTasks() ;
Console. ReadLine() ;
}
private static void GetResultsFromTasks()
{
var sumTaskViaTaskOfInt = new Task<int>(() => Sum(5) ) ;
sumTaskViaTaskOfInt. Start() ;
Console. WriteLine($"Result from sumTask is
{sumTaskViaTaskOfInt. Result}" ) ;
var sumTaskViaFactory = Task. Factory. StartNew<int>(() =>
Sum(5) ) ;
Console. WriteLine($"Result from sumTask is
{sumTaskViaFactory. Result}") ;
var sumTaskViaTaskRun = Task. Run<int>(() => Sum(5) ) ;
Console. WriteLine($"Result from sumTask is
{sumTaskViaTaskRun. Result}") ;
var sumTaskViaTaskResult = Task. FromResult<int>(Sum(5) ) ;
Console. WriteLine($"Result from sumTask is
{sumTaskViaTaskResult. Result}") ;
}
private static int Sum(int n)
{
int sum = 0;
for (int i = 0; i < n; i++)
{

sum += i;
}
return sum;
}
}
}
As shown in the preceding code, we have created tasks using generic variants. Once they
finished, we were able to get the results using the result property:
In the next section, we will learn about how we can cancel tasks.

从已完成的任务中获取结果
要从任务中返回值,TPL提供了我们之前定义的所有类的泛型变体:
Task<T>
Task.Factory.StartNew<T>
Task.Run<T>
一旦任务完成,我们应该能够通过访问Task.Result属性来获取结果。让我们通过一些代码示例来理解这一点。我们将创建各种任务并尝试在完成时从它们返回值:
```csharp
using System;
using System.Threading.Tasks;
namespace Ch02
{
class _2GettingResultFromTasks
{
static void Main(string[] args)
{
GetResultsFromTasks();
Console.ReadLine();
}
private static void GetResultsFromTasks()
{
var sumTaskViaTaskOfInt = new Task<int>(() => Sum(5));
sumTaskViaTaskOfInt.Start();
Console.WriteLine($"Result from sumTask is
{sumTaskViaTaskOfInt.Result}");
var sumTaskViaFactory = Task.Factory.StartNew<int>(() =>
Sum(5));
Console.WriteLine($"Result from sumTask is
{sumTaskViaFactory.Result}");
var sumTaskViaTaskRun = Task.Run<int>(() => Sum(5));
Console.WriteLine($"Result from sumTask is
{sumTaskViaTaskRun.Result}");
var sumTaskViaTaskResult = Task.FromResult<int>(Sum(5));
Console.WriteLine($"Result from sumTask is
{sumTaskViaTaskResult.Result}");
}
private static int Sum(int n)
{
int sum = 0;
for (int i = 0; i < n; i++)
{
sum += i;
}
return sum;
}
}
}
```
如前所述的代码所示,我们使用泛型变体创建了任务。一旦它们完成,我们就能够使用result属性获取结果:


在下一节中,我们将学习如何取消任务。

How to cancel tasks
Another important function of the TPL is to equip developers with ready-made data
structures to cancel running tasks. Those of you that have a classic threading background
will be aware of how difficult it used to be to make threads support canceling with all the
custom home-grown logic, but this is no longer the case. The .NET Framework provides
two classes to support task cancellation:
CancellationTokenSource : This class is responsible for creating cancellation
tokens and passing the cancellation request to all the tokens that were created via
the source
CancellationToken : This class is used by listeners to monitor the current state
of a request
To create tasks that can be canceled, we need to perform the following steps:
Create an instance of the System. Threading. CancellationTokenSource 1.
class, which further provides a System. Threading. CancellationToken via
the Token Property .
Pass the token while creating the task. 2.
When required, call the Cancel() method on the CancellationTokenSource . 3.
Let's try to understand how to create a token and how to pass it to the task.

如何取消任务
TPL的另一个重要功能是为开发人员提供现成的数据结构,以取消正在运行的任务。那些具有经典线程背景的人会意识到,在过去要使线程支持取消是多么困难,需要所有自定义的本土逻辑,但现在不再是这样了。.NET Framework提供了两个类来支持任务取消:
CancellationTokenSource:这个类负责创建取消令牌,并将取消请求传递给通过该源创建的所有令牌。
CancellationToken:这个类被监听器用来监视请求的当前状态。
要创建可以被取消的任务,我们需要执行以下步骤:
1. 创建一个System.Threading.CancellationTokenSource类的实例,该类进一步通过Token属性提供一个System.Threading.CancellationToken。
2. 在创建任务时传递令牌。
3. 当需要时,调用CancellationTokenSource上的Cancel()方法。
让我们尝试理解如何创建令牌以及如何将其传递给任务。

Creating a token
Tokens can be created using the following code:
CancellationTokenSource tokenSource = new CancellationTokenSource() ;
CancellationToken token = tokenSource. Token;
First, we created a tokenSource using the CancellationTokenSource constructor. Then,
we got our token using the token property of tokenSource .

创建令牌
可以使用以下代码创建令牌:
```csharp
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
```
首先,我们使用CancellationTokenSource构造函数创建了一个tokenSource。然后,我们通过tokenSource的Token属性获取了我们的令牌。

Creating a task using tokens
We can create a task by passing CancellationToken as the second argument to the task
constructor, as follows:
var sumTaskViaTaskOfInt = new Task<int>(() => Sum(5) , token) ;
var sumTaskViaFactory = Task. Factory. StartNew<int>(() => Sum(5) , token) ;
var sumTaskViaTaskRun = Task. Run<int>(() => Sum(5) , token) ;
In the classic threading model, we used to call the Abort() method on a thread that was
non-deterministic. This would stop the thread abruptly, thereby leaking memory if
resources were unmanaged. With TPL, we can call the Cancel method, which is a
cancellation token source that will, in turn, set up the IsCancellationRequested
property on the token. The underlying method that's being executed by the task should
watch for this property and should exit gracefully if it is set.
There are various ways of keeping a watch of whether the token source has requested a
cancellation:
Polling the status of the IsCancellationRequested property on the token
Registering for a request cancellation callback

使用令牌创建任务
我们可以通过将CancellationToken作为第二个参数传递给任务构造函数来创建一个任务,如下所示:
```csharp
var sumTaskViaTaskOfInt = new Task<int>(() => Sum(5), token);
var sumTaskViaFactory = Task.Factory.StartNew<int>(() => Sum(5), token);
var sumTaskViaTaskRun = Task.Run<int>(() => Sum(5), token);
```
在经典的线程模型中,我们常常在一个不确定的线程上调用Abort()方法。这将突然停止线程,从而可能导致内存泄漏,如果资源是未管理的。有了TPL,我们可以调用Cancel方法,这是一个取消令牌源,它将反过来设置令牌上的IsCancellationRequested属性。任务正在执行的底层方法应该监视此属性,并且如果设置了该属性,应该优雅地退出。
有各种方法可以监视令牌源是否请求了取消:
- 轮询令牌上的IsCancellationRequested属性的状态
- 注册请求取消回调

Polling the status of the token via the
IsCancellationRequested property
This approach is handy in scenarios that involve recursive methods or methods that contain
long-running computational logic via loops. Within our method or loops, we write code
that polls IsCancellationRequested at certain optimal intervals. If it is set, it breaks the
loop by calling the ThrowIfCancellationRequested method of the token class.

The following code is an example of canceling a task by polling the token:
private static void CancelTaskViaPoll()
{
CancellationTokenSource cancellationTokenSource =
new CancellationTokenSource() ;
CancellationToken token = cancellationTokenSource. Token;
var sumTaskViaTaskOfInt = new Task(() =>
LongRunningSum(token) , token) ;
sumTaskViaTaskOfInt. Start() ;
//Wait for user to press key to cancel task
Console. ReadLine() ;
cancellationTokenSource. Cancel() ;
}
private static void LongRunningSum(CancellationToken token)
{
for (int i = 0; i < 1000; i++)
{
//Simulate long running operation
Task. Delay(100) ;
if (token. IsCancellationRequested)
token. ThrowIfCancellationRequested() ;
}
}
In the preceding code, we created a cancellation token via the CancellationTokenSource
class. Then, we created a task by passing the token. The task executes a long-running
method, LongRunningSum (simulated), which keeps polling for
the IsCancellationRequested property of the token. It throws an exception if the user
has called cancellationTokenSource. Cancel() before the method finishes.
Polling doesn't come with any significant performance overhead and can
be used according to your requirements. Use it when you have full control
over the work that's performed by the task, such as if it's core logic that
you wrote yourself.

通过轮询令牌的IsCancellationRequested属性的状态
这种方法在涉及递归方法或包含通过循环进行长时间运行计算逻辑的方法的场景中很方便。在我们的方法或循环中,我们编写代码以一定的最优间隔轮询IsCancellationRequested。如果设置了该属性,它通过调用令牌类的ThrowIfCancellationRequested方法来中断循环。

以下代码是通过轮询令牌来取消任务的示例:
```csharp
private static void CancelTaskViaPoll()
{
    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    CancellationToken token = cancellationTokenSource.Token;
    var sumTaskViaTaskOfInt = new Task(() => LongRunningSum(token), token);
    sumTaskViaTaskOfInt.Start();
    //等待用户按键以取消任务
    Console.ReadLine();
    cancellationTokenSource.Cancel();
}
private static void LongRunningSum(CancellationToken token)
{
    for (int i = 0; i < 1000; i++)
    {
        //模拟长时间运行操作
        Task.Delay(100);
        if (token.IsCancellationRequested)
            token.ThrowIfCancellationRequested();
    }
}
```
在上述代码中,我们通过CancellationTokenSource类创建了一个取消令牌。然后,我们通过传递令牌创建了一个任务。任务执行一个长时间运行的方法,LongRunningSum(模拟的),它不断轮询令牌的IsCancellationRequested属性。如果用户在方法完成之前调用了cancellationTokenSource.Cancel(),它将抛出异常。
轮询不会带来任何显著的性能开销,并且可以根据您自己的需求使用。当您完全控制任务执行的工作,例如如果是自己编写的核心逻辑时,使用它。

Registering for a request cancellation using the
Callback delegate
This approach makes use of a Callback delegate that gets invoked when the cancellation is
requested by the underlying token. We should use this with operations that are blocked in
a way that makes it not possible to check the value of CancellationToken in a regular
fashion.

Let's have a look at the following code, which downloads files from a remote URL:
private static void DownloadFileWithoutToken()
{
WebClient webClient = new WebClient() ;
webClient. DownloadStringAsync(new
Uri("http: //www. google. com") ) ;
webClient. DownloadStringCompleted += (sender, e) =>
{
if (! e. Cancelled)
Console. WriteLine("Download Complete. ") ;
else
Console. WriteLine("Download Cancelled. ") ;
};
}
As you can see from the preceding method, once we call the DownloadStringAsync
method of WebClient , the control leaves the user. Although the WebClient class allows us
to cancel the task via the webClient. CancelAsync() method, we don't have any control
over when to invoke that.
The preceding code can be modified to make use of a Callback delegate so that we can
gain more control over task cancellation, as follows:
static void Main(string[] args)
{
CancellationTokenSource cancellationTokenSource = new
CancellationTokenSource() ;
CancellationToken token = cancellationTokenSource. Token;
DownloadFileWithToken(token) ;
//Random delay before we cancel token
Task. Delay(2000) ;
cancellationTokenSource. Cancel() ;
Console. ReadLine() ;
}
private static void DownloadFileWithToken(CancellationToken token)
{
WebClient webClient = new WebClient() ;
//Here we are registering callback delegate that will get called
//as soon as user cancels token
token. Register(() => webClient. CancelAsync() ) ;
webClient. DownloadStringAsync(new
Uri("http: //www. google. com") ) ;
webClient. DownloadStringCompleted += (sender, e) => {
//Wait for 3 seconds so we have enough time to cancel task
Task. Delay(3000) ;
if (! e. Cancelled)
Console. WriteLine("Download Complete. ") ;
else
Console. WriteLine("Download Cancelled. ") ; };
}
As you can see, in this modified version, we passed a cancellation token and subscribed to
the cancellation callback via the Register method.
As soon as the user calls the cancellationTokenSource. Cancel() method, it will cancel
the download operation by calling webClient. CancelAsync() .
CancellationTokenSource works well with the legacy
ThreadPool. QueueUserWorkItem as well.
Here is code that creates a CancellationTokenSource that can be passed to ThreadPool to
support cancellation:
// Create the token source.
CancellationTokenSource cts = new CancellationTokenSource() ;
// Pass the token to the cancellable operation.
ThreadPool. QueueUserWorkItem(new WaitCallback(DoSomething) , cts. Token) ;
In this section, we discussed various ways of canceling tasks. Canceling tasks can really
save us a lot of CPU time in cases where tasks may have become redundant. For example,
say we have created multiple tasks to sort a list of numbers using different algorithms.
Although all the algorithms will return the same result (a sorted list of numbers), we are
interested in getting results as fast as we can. We will accept the result for the first (fastest)
algorithm and cancel the rest to improve system performance. In the next section, we will
discuss how to wait on running tasks.

使用Callback委托注册请求取消
这种方法利用了一个回调委托(Callback delegate),当底层令牌请求取消时,该委托会被调用。我们应该在以下情况下使用它:操作被阻塞的方式使我们无法以常规方式检查CancellationToken的值。

让我们看一下下面的代码,它从远程URL下载文件:
```csharp
private static void DownloadFileWithoutToken()
{
    WebClient webClient = new WebClient();
    webClient.DownloadStringAsync(new Uri("http://www.google.com"));
    webClient.DownloadStringCompleted += (sender, e) =>
    {
        if (!e.Cancelled)
            Console.WriteLine("Download Complete.");
        else
            Console.WriteLine("Download Cancelled.");
    };
}
```
如前所述方法所示,一旦我们调用WebClient的DownloadStringAsync方法,控制权就会离开用户。尽管WebClient类允许我们通过webClient.CancelAsync()方法取消任务,但我们无法控制何时调用它。
前面的代码可以修改为使用回调委托,以便我们可以更好地控制任务取消,如下所示:
```csharp
static void Main(string[] args)
{
    CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    CancellationToken token = cancellationTokenSource.Token;
    DownloadFileWithToken(token);
    //在我们取消令牌之前的随机延迟
    Task.Delay(2000);
    cancellationTokenSource.Cancel();
    Console.ReadLine();
}
private static void DownloadFileWithToken(CancellationToken token)
{
    WebClient webClient = new WebClient();
    //在这里我们注册了回调委托,一旦用户取消了令牌,就会调用它
    token.Register(() => webClient.CancelAsync());
    webClient.DownloadStringAsync(new Uri("http://www.google.com"));
    webClient.DownloadStringCompleted += (sender, e) => {
        //等待3秒,以便我们有足够的时间取消任务
        Task.Delay(3000);
        if (!e.Cancelled)
            Console.WriteLine("Download Complete.");
        else
            Console.WriteLine("Download Cancelled.");
    };
}
```
如您所见,在这个修改后的版本中,我们传递了一个取消令牌,并通过Register方法订阅了取消回调。
一旦用户调用cancellationTokenSource.Cancel()方法,它将通过调用webClient.CancelAsync()来取消下载操作。
CancellationTokenSource也可以很好地与旧版的ThreadPool.QueueUserWorkItem一起使用。
以下是创建一个可以传递给ThreadPool以支持取消的CancellationTokenSource的代码:
//创建令牌源。
CancellationTokenSource cts = new CancellationTokenSource();
//将令牌传递给可取消操作。
ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomething), cts.Token);
在本节中,我们讨论了取消任务的各种方法。取消任务确实可以在任务可能变得多余的情况下节省大量的CPU时间。例如,假设我们创建了多个任务,使用不同的算法对数字列表进行排序。尽管所有算法都会返回相同的结果(一个排序后的数字列表),但我们感兴趣的是尽快获得结果。我们将接受第一个(最快)算法的结果,并取消其余算法以提高系统性能。在下一节中,我们将讨论如何等待正在运行的任务。

How to wait on running tasks
In the previous examples, we called the Task. Result property to get a result from a
completed task. This blocks the calling thread until a result is available. TPL provides
another way for us to wait on one or more tasks.
There are various APIs available in TPL so that we can wait on one or more tasks. These are
as follows:
Task. Wait
Task. WaitAll
Task. WaitAny
Task. WhenAll
Task. WhenAny
These APIs will be defined in the following subsections.

如何等待正在运行的任务
在之前的示例中,我们调用了Task.Result属性来获取已完成任务的结果。这会阻塞调用线程,直到结果可用。TPL为我们提供了另一种等待一个或多个任务的方式。
TPL中有各种API可供我们等待一个或多个任务。这些如下:
- Task.Wait
- Task.WaitAll
- Task.WaitAny
- Task.WhenAll
- Task.WhenAny
这些API将在以下小节中定义。

Task.Wait
This is an instance method that can be used to wait on a single task. We can specify the
maximum amount of time for which the caller will wait for the task to complete before
unblocking itself with a timeout exception. We can also have full control over monitoring
events that have been canceled by passing a cancellation token to the method. The calling
method will be blocked until the thread either completes, is canceled, or throws an
exception:
var task = Task. Factory. StartNew(() => Console. WriteLine("Inside Thread") ) ;
//Blocks the current thread until task finishes.
task. Wait() ;
There are five overloaded versions of the Wait method:
Wait() : Waits indefinitely for the task to finish. The calling thread is blocked
until the child thread has finished.
Wait(CancellationToken) : Waits for the task to finish execution indefinitely
or when the cancellation token is canceled.
Wait(int) : Waits for the task to finish execution within a specified period of
time, in milliseconds.
Wait(TimeSpan) : Waits for the task to finish execution within a specified time
interval.
Wait(int, CancellationToken) : Waits for the task to finish execution within
a specified period of time, in milliseconds, or when the cancellation token is
canceled.

Task.Wait
这是一个实例方法,可用于等待单个任务。我们可以指定调用者在解除阻塞自身并抛出超时异常之前,等待任务完成的最大时间。我们还可以通过传递取消令牌来完全控制监视被取消的事件。调用方法将被阻塞,直到线程完成、被取消或抛出异常为止:
```csharp
var task = Task.Factory.StartNew(() => Console.WriteLine("Inside Thread"));
// 阻塞当前线程,直到任务完成
task.Wait();
```
Wait方法有五个重载版本:
- Wait():无限期等待任务完成。调用线程将被阻塞,直到子线程完成。
- Wait(CancellationToken):无限期等待任务完成,或者当取消令牌被取消时。
- Wait(int):在指定的时间段内(以毫秒为单位)等待任务完成。
- Wait(TimeSpan):在指定的时间间隔内等待任务完成。
- Wait(int, CancellationToken):在指定的时间段内(以毫秒为单位)等待任务完成,或者当取消令牌被取消时。

Task.WaitAll
This is a static method that is defined in the Task class and used to wait on multiple tasks.
The tasks are passed as an array to the method and the caller is blocked until all the tasks
are completed. This method also supports timeout and cancellation tokens. Some example
code that uses this method is as follows:
Task taskA = Task. Factory. StartNew(() =>
Console. WriteLine("TaskA finished") ) ;
Task taskB = Task. Factory. StartNew(() =>
Console. WriteLine("TaskB finished") ) ;
Task. WaitAll(taskA, taskB) ;
Console. WriteLine("Calling method finishes") ;
The output of the preceding code is as follows:
As you can see, the Calling method finishes statement is executed when both tasks have
finished executing.
An example use case of this method might be when we need data from multiple sources
(we have one task for each source) and we want to combine the data from all the tasks so
that they can be displayed on the UI.

Task.WaitAll
这是一个在Task类中定义的静态方法,用于等待多个任务。
任务作为数组传递给该方法,调用者被阻塞,直到所有任务完成。此方法还支持超时和取消令牌。以下是使用此方法的一些示例代码:
```csharp
Task taskA = Task.Factory.StartNew(() => Console.WriteLine("TaskA finished"));
Task taskB = Task.Factory.StartNew(() => Console.WriteLine("TaskB finished"));
Task.WaitAll(taskA, taskB);
Console.WriteLine("Calling method finishes");
```
上述代码的输出如下:

如您所见,当两个任务都执行完毕后,"Calling method finishes"语句才会被执行。
这个方法的一个用例可能是当我们需要从多个来源获取数据(每个来源都有一个任务)并且我们希望将所有任务的数据组合起来以便在UI上显示。

Task.WaitAny
This is another static method that is defined in the Task class. Just like WaitAll , WaitAny
is used to wait on multiple tasks, but the caller is unblocked as soon as any of the tasks that
are passed as arrays to the method finish executing. Like the other methods, WaitAny
supports the timeout and cancellation tokens. Some example code that uses this method is
as follows:
Task taskA = Task. Factory. StartNew(() =>
Console. WriteLine("TaskA finished") ) ;
Task taskB = Task. Factory. StartNew(() =>
Console. WriteLine("TaskB finished") ) ;
Task. WaitAny(taskA, taskB) ;
Console. WriteLine("Calling method finishes") ;
In the preceding code, we started two tasks and waited on them using WaitAny . This
method blocks the current thread. As soon as any of the tasks complete, the calling thread is
unblocked.
An example use case of this method might be when the data we require is available from
different sources and we need it as quickly as possible. Here, we create tasks that make
requests to different sources. As soon as any of the tasks finish, we will unblock the calling
thread and get the result from the finished task.

Task.WaitAny
这是在Task类中定义的另一个静态方法。就像WaitAll一样,WaitAny用于等待多个任务,但是一旦作为数组传递给该方法的任何任务执行完毕,调用者就会被解除阻塞。与其他方法一样,WaitAny支持超时和取消令牌。以下是使用此方法的一些示例代码:
```csharp
Task taskA = Task.Factory.StartNew(() => Console.WriteLine("TaskA finished"));
Task taskB = Task.Factory.StartNew(() => Console.WriteLine("TaskB finished"));
Task.WaitAny(taskA, taskB);
Console.WriteLine("Calling method finishes");
```
在上述代码中,我们启动了两个任务并使用WaitAny对它们进行等待。此方法会阻塞当前线程。只要任何任务完成,调用线程就会被解除阻塞。
这个方法的一个用例可能是当我们需要尽快从不同来源获取数据时。在这里,我们创建了向不同源发出请求的任务。只要任何任务完成,我们就将解除调用线程的阻塞,并从已完成的任务中获取结果。

Task.WhenAll
This is a non-blocking variant of the WaitAll method. It returns a task that represents a
waiting action for all of the specified tasks. Unlike WaitAll , which blocks the calling
thread, WhenAll can be awaited inside an asynchronous method, thus freeing up the
calling thread to perform other operations. Some example code that uses this method is as
follows:
Task taskA = Task. Factory. StartNew(() =>
Console. WriteLine("TaskA finished") ) ;
Task taskB = Task. Factory. StartNew(() =>
Console. WriteLine("TaskB finished") ) ;
Task. WhenAll(taskA, taskB) ;
Console. WriteLine("Calling method finishes") ;
This code works the same way as Task. WaitAll , apart from the fact that the calling thread
returns to the ThreadPool instead of being blocked.

Task.WhenAll
这是WaitAll方法的一个非阻塞变体。它返回一个表示等待所有指定任务完成的任务。与阻塞调用线程的WaitAll不同,WhenAll可以在异步方法内被await,从而释放调用线程去执行其他操作。以下是使用此方法的一些示例代码:
```csharp
Task taskA = Task.Factory.StartNew(() => Console.WriteLine("TaskA finished"));
Task taskB = Task.Factory.StartNew(() => Console.WriteLine("TaskB finished"));
await Task.WhenAll(taskA, taskB);
Console.WriteLine("Calling method finishes");
```
这段代码的工作方式与Task.WaitAll相同,除了调用线程返回到ThreadPool而不是被阻塞的事实。

Task.WhenAny
This is a non-blocking variant of WaitAny . It returns a task that encapsulates a waiting
action on a single underlying task. Unlike WaitAny , it doesn't block the calling thread. The
calling thread can call await on it inside an asynchronous method. Some example code that
uses this method is as follows:
Task taskA = Task. Factory. StartNew(() =>
Console. WriteLine("TaskA finished") ) ;
Task taskB = Task. Factory. StartNew(() =>
Console. WriteLine("TaskB finished") ) ;
Task. WhenAny(taskA, taskB) ;
Console. WriteLine("Calling method finishes") ;
This code works the same way as Task. WaitAny , apart from the fact that the calling thread
returns to the ThreadPool instead of being blocked.
In this section, we discussed how to write efficient code while working with multiple
threads without code branching. Code flow looks synchronous though it works in parallel
wherever required. In the next section, we will learn about how tasks deal with exceptions.

Task.WhenAny
这是WaitAny的一个非阻塞变体。它返回一个封装了对单个底层任务等待操作的任务。与WaitAny不同,它不会阻塞调用线程。调用线程可以在异步方法内对它调用await。以下是使用此方法的一些示例代码:
```csharp
Task taskA = Task.Factory.StartNew(() => Console.WriteLine("TaskA finished"));
Task taskB = Task.Factory.StartNew(() => Console.WriteLine("TaskB finished"));
await Task.WhenAny(taskA, taskB);
Console.WriteLine("Calling method finishes");
```
这段代码的工作方式与Task.WaitAny相同,除了调用线程返回到ThreadPool而不是被阻塞的事实。
在这一节中,我们讨论了如何在处理多线程时编写高效代码而不进行代码分支。代码流看起来是同步的,尽管它在需要的地方并行工作。在下一节中,我们将学习任务如何处理异常。

Handling task exceptions
Exception handling is one of the most important aspects of parallel programming. All good
clean code practitioners focus on handling exceptions efficiently. This becomes even more
important with parallel programming as any unhandled exceptions in threads or tasks can
cause the application to crash abruptly. Fortunately, TPL provides a nice, efficient design to
handle and manage exceptions. Any unhandled exceptions that occur in a task are deferred
and then propagated to a joining thread, which observes the task for exceptions.
Any exception that occurs inside a task is always wrapped under
the AggregateException class and returned to the caller that is observing the exceptions.
If the caller is waiting on a single task, the inner exception property of the
AggregateException class will return the original exception. If the caller is waiting for
multiple tasks, however, such as Task. WaitAll , Task. WhenAll , Task. WaitAny , or
Task. WhenAny , all the exceptions that occur from tasks are returned to the caller as a
collection. They are accessible via the InnerExceptions property.
Now, let's look at the various ways we can handle exceptions inside tasks.

处理任务异常
在并行编程中,异常处理是最重要的方面之一。所有优秀的代码实践者都专注于高效地处理异常。这在并行编程中变得更加重要,因为线程或任务中的任何未处理的异常都可能导致应用程序突然崩溃。幸运的是,TPL提供了一种很好的、高效的设计来处理和管理异常。任务中发生的任何未处理的异常都会被推迟,然后传播到连接线程,该线程会观察任务是否有异常。
任务内部发生的任何异常总是被封装在AggregateException类下,并返回给观察异常的调用者。如果调用者正在等待单个任务,AggregateException类的InnerException属性将返回原始异常。然而,如果调用者在等待多个任务,例如Task.WaitAll、Task.WhenAll、Task.WaitAny或Task.WhenAny,那么所有从任务发生的异常都会作为一个集合返回给调用者。它们可以通过InnerExceptions属性访问。
现在,让我们看看我们可以在任务内部处理异常的各种方式。

Handling exception from single tasks
In the following code, we're creating a simple task that tries to divide a number by 0,
thereby causing a DivideByZeroException . The exception is returned to the caller and
handled inside the catch block. Since it's a single task, the exception object is wrapped
under the InnerException property of the AggregateException object:
class _4HandlingExceptions
{
static void Main(string[] args)
{
Task task = null;
try
{
task = Task. Factory. StartNew(() =>

{
int num = 0, num2 = 25;
var result = num2 / num;
}) ;
task. Wait() ;
}
catch (AggregateException ex)
{
Console. WriteLine($"Task has finished with
exception {ex. InnerException. Message}") ;
}
Console. ReadLine() ;
}
}
The following is the output when we run the preceding code:

处理单个任务的异常
在下面的代码中,我们创建了一个简单的任务,尝试将一个数字除以0,从而引发DivideByZeroException。异常被返回给调用者并在catch块中处理。由于是单个任务,异常对象被封装在AggregateException对象的InnerException属性下:

```csharp
class _4HandlingExceptions
{
    static void Main(string[] args)
    {
        Task task = null;
        try
        {
            task = Task.Factory.StartNew(() =>
            {
                int num = 0, num2 = 25;
                var result = num2 / num;
            });
            task.Wait();
        }
        catch (AggregateException ex)
        {
            Console.WriteLine($"Task has finished with exception {ex.InnerException.Message}");
        }
        Console.ReadLine();
    }
}
```
当我们运行上述代码时,输出结果如下:

Handling exceptions from multiple tasks
Now, we'll create multiple tasks and then try to throw exceptions from them. Then, we'll
learn how to list different exceptions from different tasks from the caller:
static void Main(string[] args)
{
Task taskA = Task. Factory. StartNew(() => throw
new DivideByZeroException() ) ;
Task taskB = Task. Factory. StartNew(() => throw
new ArithmeticException() ) ;
Task taskC = Task. Factory. StartNew(() => throw
new NullReferenceException() ) ;
try
{
Task. WaitAll(taskA, taskB, taskC) ;
}
catch (AggregateException ex)
{
foreach (Exception innerException in ex. InnerExceptions)
{
Console. WriteLine(innerException. Message) ;
}
}

Console. ReadLine() ;
}
Here is the output when we run the preceding code:
In the preceding code, we created three tasks that throw different exceptions and all
threads are awaited using Task. WaitAll . As you can see, the exceptions are observed by
calling WaitAll and not just by starting the task, which is why we wrapped WaitAll
inside the try block. The WaitAll method will return when all the tasks that have been
passed to it have faulted by throwing exceptions and the corresponding catch block is
executed. We can find all the exceptions that originated from all the tasks by iterating over
the InnerExceptions property of the AggregateException class.

处理多个任务的异常
现在,我们将创建多个任务,然后尝试从它们中抛出异常。然后,我们将学习如何从调用者列出来自不同任务的不同异常:

```csharp
static void Main(string[] args)
{
    Task taskA = Task.Factory.StartNew(() => throw new DivideByZeroException());
    Task taskB = Task.Factory.StartNew(() => throw new ArithmeticException());
    Task taskC = Task.Factory.StartNew(() => throw new NullReferenceException());
    try
    {
        Task.WaitAll(taskA, taskB, taskC);
    }
    catch (AggregateException ex)
    {
        foreach (Exception innerException in ex.InnerExceptions)
        {
            Console.WriteLine(innerException.Message);
        }
    }

    Console.ReadLine();
}
```
当我们运行上述代码时,输出结果如下:


在上述代码中,我们创建了三个抛出不同异常的任务,并使用Task.WaitAll等待所有线程。如您所见,通过调用WaitAll观察到异常,而不仅仅是启动任务,这就是为什么我们在try块中包装WaitAll的原因。当传递给它的所有任务都通过抛出异常而出错时,WaitAll方法将返回,并且相应的catch块将被执行。我们可以通过遍历AggregateException类的InnerExceptions属性来找到所有来自所有任务的原始异常。

Handling task exceptions with a callback function
Another option to find out about these exceptions is to use the callback function to access
and handle the exceptions that originate from tasks:
static void Main(string[] args)
{
Task taskA = Task. Factory. StartNew(() => throw
new DivideByZeroException() ) ;
Task taskB = Task. Factory. StartNew(() => throw
new ArithmeticException() ) ;
Task taskC = Task. Factory. StartNew(() => throw
new NullReferenceException() ) ;
try
{
Task. WaitAll(taskA, taskB, taskC) ;
}
catch (AggregateException ex)
{
ex. Handle(innerException =>
{
Console. WriteLine(innerException. Message) ;
return true;
}) ;
}

Console. ReadLine() ;
}
Here is the output when we run the preceding code in Visual Studio:
As shown in the preceding code, rather than integrating over InnerExceptions , we have
subscribed to the handle callback function on AggregateException . This is fired for all
the tasks that throw the exception and we can return true , indicating that the exception
has been handled gracefully.

使用回调函数处理任务异常
另一个找出这些异常的选项是使用回调函数来访问和处理来自任务的异常:

```csharp
static void Main(string[] args)
{
    Task taskA = Task.Factory.StartNew(() => throw new DivideByZeroException());
    Task taskB = Task.Factory.StartNew(() => throw new ArithmeticException());
    Task taskC = Task.Factory.StartNew(() => throw new NullReferenceException());
    try
    {
        Task.WaitAll(taskA, taskB, taskC);
    }
    catch (AggregateException ex)
    {
        ex.Handle(innerException =>
        {
            Console.WriteLine(innerException.Message);
            return true;
        });
    }

    Console.ReadLine();
}
```
当我们在Visual Studio中运行上述代码时,输出结果如下:


如前所述代码所示,我们没有集成遍历InnerExceptions,而是在AggregateException上订阅了handle回调函数。这对所有抛出异常的任务都会触发,我们可以返回true,表示异常已经被优雅地处理。

Converting APM patterns into tasks
The legacy APM approach used the IAsyncResult interface to create asynchronous
methods with a design pattern using two methods: BeginMethodName and
EndMethodName . Let's try to understand the journey of a program from being synchronous,
to an APM, and then to a task.
Here is a synchronous method that reads data from a text file:
private static void ReadFileSynchronously()
{
string path = @"Test. txt";
//Open the stream and read content.
using (FileStream fs = File. OpenRead(path) )
{
byte[] b = new byte[1024] ;
UTF8Encoding encoder = new UTF8Encoding(true) ;
fs. Read(b, 0, b. Length) ;
Console. WriteLine(encoder. GetString(b) ) ;
}
}

There is nothing fancy in the preceding code. First, we created a FileStream object and
called the Read method, which reads the file from the disk synchronously into a buffer and
then writes the buffer to the console. We converted the buffer into a string using the
UTF8Encoding class. The problem with this approach, however, is that the moment a call
to Read is made, the thread is blocked until the read operation has finished. I/O operations
are managed by the CPU using CPU cycles, so there is no point in keeping the thread
waiting for the I/O operation to complete. Let's try to understand the APM way of doing
this:
private static void ReadFileUsingAPMAsyncWithoutCallback()
{
string filePath = @"Test. txt";
//Open the stream and read content.
using (FileStream fs = new FileStream(filePath,
FileMode. Open, FileAccess. Read, FileShare. Read,
1024, FileOptions. Asynchronous) )
{
byte[] buffer = new byte[1024] ;
UTF8Encoding encoder = new UTF8Encoding(true) ;
IAsyncResult result = fs. BeginRead(buffer, 0,
buffer. Length, null, null) ;
Console. WriteLine("Do Something here") ;
int numBytes = fs. EndRead(result) ;
fs. Close() ;
Console. WriteLine(encoder. GetString(buffer) ) ;
}
}
As shown in the preceding code, we have replaced the synchronous Read method with an
asynchronous version, that is, BeginRead . The moment the compiler encounters
BeginRead , an instruction is sent to the CPU to start reading the file and the thread is
unblocked. We can perform other tasks in the same method before blocking the thread
again by calling EndRead to wait for the Read operation to finish and collect the result. This
is a simple yet efficient approach in order to make responsive applications, though we are
also blocking the thread to fetch results. Rather than calling EndRead in the same method,
we can make use of Overload , which accepts a callback method that gets called
automatically when the read operation finishes, to avoid blocking the thread. The signature
of this method is as follows:
public override IAsyncResult BeginRead(
byte[] array,
int offset,
int numBytes,
AsyncCallback userCallback,
object stateObject)

Here, we have seen how we moved from a synchronous method to APM. Now, we are
going to convert the APM implementation into a task. This is demonstrated in the
following code:
private static void ReadFileUsingTask()
{
string filePath = @"Test. txt";
//Open the stream and read content.
using (FileStream fs = new FileStream(filePath, FileMode. Open,
FileAccess. Read, FileShare. Read, 1024,
FileOptions. Asynchronous) )
{
byte[] buffer = new byte[1024] ;
UTF8Encoding encoder = new UTF8Encoding(true) ;
//Start task that will read file asynchronously
var task = Task<int>. Factory. FromAsync(fs. BeginRead,
fs. EndRead, buffer, 0, buffer. Length, null) ;
Console. WriteLine("Do Something while file is read
asynchronously") ;
//Wait for task to finish
task. Wait() ;
Console. WriteLine(encoder. GetString(buffer) ) ;
}
}
As shown in the preceding code, we replaced the BeginRead method with
Task<int>. Factory. FromAsync . This is a way of implementing a TAP. The method
returns a task, which runs in the background while we continue doing other work in the
same method, before blocking the thread again to get the results using task. Wait() . This
is how you can easily convert any APM code into TAP.

将APM模式转换为任务
传统的APM方法使用IAsyncResult接口来创建异步方法,并采用两个方法的设计模式:BeginMethodName和EndMethodName。让我们尝试理解一个程序从同步变为APM,然后变为任务的过程。

这里有一个同步读取文本文件数据的方法:
```csharp
private static void ReadFileSynchronously()
{
    string path = @"Test.txt";
    // 打开流并读取内容。
    using (FileStream fs = File.OpenRead(path))
    {
        byte[] b = new byte[1024];
        UTF8Encoding encoder = new UTF8Encoding(true);
        fs.Read(b, 0, b.Length);
        Console.WriteLine(encoder.GetString(b));
    }
}
```
上述代码没有什么特别之处。首先,我们创建了一个FileStream对象,并调用了Read方法,该方法将文件从磁盘同步读取到缓冲区中,然后将缓冲区写入控制台。我们使用UTF8Encoding类将缓冲区转换为字符串。然而,这种方法的问题是,一旦调用了Read方法,线程就会被阻塞,直到读取操作完成。I/O操作是由CPU使用CPU周期来管理的,因此没有必要让线程等待I/O操作完成。让我们尝试理解APM方式的实现:

```csharp
private static void ReadFileUsingAPMAsyncWithoutCallback()
{
    string filePath = @"Test.txt";
    // 打开流并读取内容。
    using (FileStream fs = new FileStream(filePath,
    FileMode.Open, FileAccess.Read, FileShare.Read,
    1024, FileOptions.Asynchronous))
    {
        byte[] buffer = new byte[1024];
        UTF8Encoding encoder = new UTF8Encoding(true);
        IAsyncResult result = fs.BeginRead(buffer, 0,
        buffer.Length, null, null);
        Console.WriteLine("在这里执行其他操作");
        int numBytes = fs.EndRead(result);
        fs.Close();
        Console.WriteLine(encoder.GetString(buffer));
    }
}
```
如上所示的代码,我们将同步的Read方法替换为异步版本,即BeginRead。编译器遇到BeginRead时,会向CPU发送指令开始读取文件,并且线程被解阻。我们可以在同一方法中执行其他任务,然后再通过调用EndRead来阻塞线程,等待读取操作完成并收集结果。这是一种简单而高效的方法,用于制作响应式应用程序,尽管我们也在阻塞线程以获取结果。我们可以使用重载方法,该方法接受一个回调方法,当读取操作完成时会自动调用,以避免阻塞线程。这个方法的签名如下:
```csharp
public override IAsyncResult BeginRead(
    byte[] array,
    int offset,
    int numBytes,
    AsyncCallback userCallback,
    object stateObject)
```
在这里,我们已经看到了如何从同步方法转变为APM。现在,我们将把APM实现转换为任务。这在下面的代码中进行了演示:
```csharp
private static void ReadFileUsingTask()
{
    string filePath = @"Test.txt";
    // 打开流并读取内容。
    using (FileStream fs = new FileStream(filePath, FileMode.Open,
    FileAccess.Read, FileShare.Read, 1024,
    FileOptions.Asynchronous))
    {
        byte[] buffer = new byte[1024];
        UTF8Encoding encoder = new UTF8Encoding(true);
        // 开始异步读取文件的任务
        var task = Task<int>.Factory.FromAsync(fs.BeginRead,
        fs.EndRead, buffer, 0, buffer.Length, null);
        Console.WriteLine("在文件异步读取时执行其他操作");
        // 等待任务完成
        task.Wait();
        Console.WriteLine(encoder.GetString(buffer));
    }
}
```
如上所示的代码,我们将BeginRead方法替换为Task<int>.Factory.FromAsync。这是实现TAP的一种方式。该方法返回一个任务,它在后台运行,而我们在同一方法中继续执行其他工作,然后再使用task.Wait()阻塞线程以获取结果。这就是您可以轻松地将任何APM代码转换为TAP的方式。

Converting EAPs into tasks
EAPs are used to create components that wrap expensive and time-consuming operations.
Due to this, they need to be made asynchronous. This pattern has been used in the .NET
Framework to create components such as BackgroundWorker and WebClient .
Methods that implement this pattern carry out long-running tasks asynchronously in the
background but keep notifying the user of their progress and status via events, which is
why they are known as event-based.
The following code shows an implementation of a component that uses EAP:
private static void EAPImplementation()
{
var webClient = new WebClient() ;
webClient. DownloadStringCompleted += (s, e) =>
{
if (e. Error ! = null)
Console. WriteLine(e. Error. Message) ;
else if (e. Cancelled)
Console. WriteLine("Download Cancel") ;
else
Console. WriteLine(e. Result) ;
};
webClient. DownloadStringAsync(new
Uri("http: //www. someurl. com") ) ;
}
In the preceding code, we subscribed to the DownloadStringCompleted event, which gets
fired once webClient has downloaded the file from the URL. As you can see, we tried to
read various result options, such as exception, cancellation, and result, using the if-else
construct. Converting EAP into TAP is tricky compared to APM as it requires a good
understanding of the internal nature of EAP components because we need to plug the new
code into the correct events to make it work. Let's take a look at the converted
implementation:
private static Task<string> EAPToTask()
{
var taskCompletionSource = new TaskCompletionSource<string>() ;
var webClient = new WebClient() ;
webClient. DownloadStringCompleted += (s, e) =>
{
if (e. Error ! = null)
taskCompletionSource. TrySetException(e. Error) ;
else if (e. Cancelled)
taskCompletionSource. TrySetCanceled() ;
else
taskCompletionSource. TrySetResult(e. Result) ;
};
webClient. DownloadStringAsync(new
Uri("http: //www. someurl. com") ) ;
return taskCompletionSource. Task;
}
The simplest way of converting EAP into TAP is via the TaskCompletionSource class. We
have plugged in all the scenarios and set the result, exception, or cancellation results to the
instance of the TaskCompletionSource class. Then, we returned the wrapped
implementation as a task to the user.

将EAP转换为任务

EAP用于创建封装耗时且昂贵的操作的组件。因此,它们需要被设计成异步的。这种模式已经在.NET框架中被用来创建诸如BackgroundWorker和WebClient这样的组件。实现这种模式的方法在后台异步执行长时间运行的任务,但通过事件不断通知用户它们的进度和状态,这就是为什么它们被称为基于事件的。

下面的代码展示了一个使用EAP的组件的实现:

```csharp
private static void EAPImplementation()
{
    var webClient = new WebClient();
    webClient.DownloadStringCompleted += (s, e) =>
    {
        if (e.Error != null)
            Console.WriteLine(e.Error.Message);
        else if (e.Cancelled)
            Console.WriteLine("Download Cancel");
        else
            Console.WriteLine(e.Result);
    };
    webClient.DownloadStringAsync(new Uri("http://www.someurl.com"));
}
```

在上述代码中,我们订阅了DownloadStringCompleted事件,该事件在webClient从URL下载文件后触发。如你所见,我们尝试使用if-else结构读取各种结果选项,如异常、取消和结果。将EAP转换为TAP相比APM来说更棘手,因为它需要对EAP组件的内部性质有深入的了解,因为我们需要将新代码插入到正确的事件中使其工作。让我们看一下转换后的实现:

```csharp
private static Task<string> EAPToTask()
{
    var taskCompletionSource = new TaskCompletionSource<string>();
    var webClient = new WebClient();
    webClient.DownloadStringCompleted += (s, e) =>
    {
        if (e.Error != null)
            taskCompletionSource.TrySetException(e.Error);
        else if (e.Cancelled)
            taskCompletionSource.TrySetCanceled();
        else
            taskCompletionSource.TrySetResult(e.Result);
    };
    webClient.DownloadStringAsync(new Uri("http://www.someurl.com"));
    return taskCompletionSource.Task;
}
```

将EAP转换为TAP最简单的方法是通过TaskCompletionSource类。我们已经插入了所有的情况,并将结果、异常或取消结果设置到了TaskCompletionSource类的实例。然后,我们将包装好的实现作为任务返回给用户。

More on tasks
Now, let's learn some more important concepts about tasks that might come in handy. Up
until now, we have created tasks that are independent. To create more complex solutions,
however, we sometimes need to define relationships between tasks. We can create subtasks,
child tasks, as well as continuation tasks to do this. Let's try to understand each of these
with examples. Later in this section, we will learn about thread storage and queues.

关于任务的更多内容
现在,让我们学习一些可能会派上用场的关于任务的更重要概念。到目前为止,我们已经创建了独立的任务。然而,为了构建更复杂的解决方案,我们有时需要定义任务之间的关系。我们可以创建子任务、子代任务以及延续任务来实现这一点。让我们通过示例来尝试理解这些概念。在本节后面,我们将学习线程存储和队列。

Continuation tasks
Continuation tasks work more like promises. We can make use of them when we need to
chain multiple tasks. The second task starts when the first one finishes and the result of the
first task or the exceptions are passed to the child task. We can chain more than one task to
create a long chain of tasks, or we can create a selective continuation chain with the
methods provided by TPL. The following constructs are provided by TPL for task
continuation:
Task. ContinueWith
Task. Factory. ContinueWhenAll
Task. Factory. ContinueWhenAll<T>
Task. Factory. ContinueWhenAny
Task. Factory. ContinueWhenAny<T>

延续任务

延续任务更像是承诺(promises)。当我们需要链接多个任务时,可以使用它们。第二个任务在第一个任务完成时开始,并且第一个任务的结果或异常传递给子任务。我们可以链接多个任务以创建长链的任务,或者可以使用TPL提供的方法创建选择性的延续链。TPL提供了以下构造用于任务延续:

- Task.ContinueWith
- Task.Factory.ContinueWhenAll
- Task.Factory.ContinueWhenAll<T>
- Task.Factory.ContinueWhenAny
- Task.Factory.ContinueWhenAny<T>

Continuing tasks using the Task.ContinueWith method
The continuation of a task can be easily achieved using the ContinueWith method that's
provided by TPL.
Let's try to understand simple chaining with an example:
var task = Task. Factory. StartNew<DataTable>(() =>
{
Console. WriteLine("Fetching Data") ;
return FetchData() ;
}) . ContinueWith(
(e) => {
var firstRow = e. Result. Rows[0] ;
Console. WriteLine("Id is {0} and Name is {0}",
firstRow["Id"] , firstRow["Name"] ) ;
}) ;
In the preceding example, we need to fetch and display data. The primary task calls the
FetchData method. When it has finished, the result is passed as input to the continuation
task, which takes care of printing the data. The output is as follows:
We can chain multiple tasks as well, thereby creating a chain of tasks, as shown here:
var task = Task. Factory. StartNew<int>(() => GetData() ) .
. ContinueWith((i) => GetMoreData(i. Result) ) .
. ContinueWith((j) => DisplayData(j. Result) ) ) ;
We can control when the continuation task will run by passing
the System. Threading. Tasks. TaskContinuationOptions enumeration as a parameter
that has the following options:
None : This is the default option. The continuation task will run when the primary
task has completed.
OnlyOnRanToCompletion : The continuation task will run when the primary
task has completed successfully, meaning it has not canceled or faulted.
NotOnRanToCompletion : The continuation task will run when the primary task
has been canceled or faulted.
OnlyOnFaulted : The continuation task will run only when the primary task has
faulted.
NotOnFaulted : The continuation task will run only when the primary task has
not faulted.
OnlyOnCancelled : The continuation task will run only when the primary task
has been canceled.
NotOnCancelled : The continuation task will run only when the primary task
has not been canceled.

使用Task.ContinueWith方法延续任务

使用TPL提供的ContinueWith方法可以轻松实现任务的延续。

让我们通过一个例子来理解简单的链式结构:

```csharp
var task = Task.Factory.StartNew<DataTable>(() =>
{
    Console.WriteLine("Fetching Data");
    return FetchData();
})
.ContinueWith((e) => {
    var firstRow = e.Result.Rows[0];
    Console.WriteLine("Id is {0} and Name is {1}",
        firstRow["Id"], firstRow["Name"]);
});
```

在前面的例子中,我们需要获取并显示数据。主要任务调用了FetchData方法。当它完成后,将结果作为输入传递给延续任务,该任务负责打印数据。输出如下:

我们也可以链接多个任务,从而创建一个任务链,如下所示:

```csharp
var task = Task.Factory.StartNew<int>(() => GetData())
.ContinueWith((i) => GetMoreData(i.Result))
.ContinueWith((j) => DisplayData(j.Result));
```

我们可以通过传递System.Threading.Tasks.TaskContinuationOptions枚举作为参数来控制延续任务何时运行,该枚举具有以下选项:

- None:这是默认选项。延续任务将在主任务完成时运行。
- OnlyOnRanToCompletion:延续任务将在主任务成功完成时运行,意味着它没有被取消或出错。
- NotOnRanToCompletion:延续任务将在主任务被取消或出错时运行。
- OnlyOnFaulted:延续任务仅在主任务出错时运行。
- NotOnFaulted:延续任务仅在主任务未出错时运行。
- OnlyOnCancelled:延续任务仅在主任务被取消时运行。
- NotOnCancelled:延续任务仅在主任务未被取消时运行。

Continuing tasks using Task.Factory.ContinueWhenAll
and Task.Factory.ContinueWhenAll<T>
We can wait for multiple tasks and chain a continuation code that will only run when all
the tasks are completed successfully. Let's look at an example:
private async static void ContinueWhenAll()
{
int a = 2, b = 3;
Task<int> taskA = Task. Factory. StartNew<int>(() => a * a) ;
Task<int> taskB = Task. Factory. StartNew<int>(() => b * b) ;
Task<int> taskC = Task. Factory. StartNew<int>(() => 2 * a * b) ;
var sum = await Task. Factory. ContinueWhenAll<int>(new Task[]
{ taskA, taskB, taskC }, (tasks)
=>tasks. Sum(t => (t as Task<int>) . Result) ) ;
Console. WriteLine(sum) ;
}
In the preceding code, we want to calculate a*a + b*b +2 *a *b . We break down the
task into three units: a*a , b*b , and 2*a*b . Each of these units is executed by three different
threads: taskA , taskB , and taskC . Then, we wait for all the tasks to finish and pass them
as a first parameter to the ContinueWhenAll method. When all the threads finish
executing, the continuation delegate executes, which is specified by the second parameter
to the ContinueWhenAll method. The continuation delegate sums the result of the
execution from all the threads and returns them to the caller, which is printed in the next
line.

使用Task.Factory.ContinueWhenAll和Task.Factory.ContinueWhenAll<T>延续任务

我们可以等待多个任务,并在所有任务成功完成后链接一个延续代码。让我们看一个例子:

```csharp
private async static void ContinueWhenAll()
{
    int a = 2, b = 3;
    Task<int> taskA = Task.Factory.StartNew<int>(() => a * a);
    Task<int> taskB = Task.Factory.StartNew<int>(() => b * b);
    Task<int> taskC = Task.Factory.StartNew<int>(() => 2 * a * b);
    var sum = await Task.Factory.ContinueWhenAll<int>(new Task[]
    { taskA, taskB, taskC }, (tasks)
    => tasks.Sum(t => (t as Task<int>).Result));
    Console.WriteLine(sum);
}
```

在上述代码中,我们想要计算a*a + b*b + 2*a*b。我们将任务分解为三个单元:a*a、b*b和2*a*b。这些单元由三个不同的线程执行:taskA、taskB和taskC。然后,我们等待所有任务完成,并将它们作为第一个参数传递给ContinueWhenAll方法。当所有线程执行完毕后,由ContinueWhenAll方法的第二个参数指定的延续委托执行。延续委托将所有线程的执行结果求和并返回给调用者,下一行打印出来。

Continuing tasks using
Task.Factory.ContinueWhenAny and
Task.Factory.ContinueWhenAny<T>
We can wait for multiple tasks and chains in continuation code that will run when any of
the tasks are completed successfully:
private static void ContinueWhenAny()
{
int number = 13;
Task<bool> taskA = Task. Factory. StartNew<bool>(() =>
number / 2 ! = 0) ;
Task<bool> taskB = Task. Factory. StartNew<bool>(() =>
(number / 2) * 2 ! = number) ;
Task<bool> taskC = Task. Factory. StartNew<bool>(() =>
(number & 1) ! = 0) ;
Task. Factory. ContinueWhenAny<bool>(new Task<bool>[]
{ taskA, taskB, taskC }, (task) =>
{
Console. WriteLine((task as Task<bool>) . Result) ;
}
) ;
}
As shown in the preceding code, we have three different pieces of logic to find out whether
a number is odd. Let's assume that we don't know which of these pieces of logic is going to
be the fastest. To calculate the result, we create three tasks, each of which encapsulates a
different odd-number-finding logic, and run them concurrently. Since a number can be
either odd or even at a time, the result from all the threads will be the same and will differ
in terms of their speed of execution. Due to this, it makes sense to just get the first result
and discard the rest. This is what we have achieved using the ContinueWhenAny method.

使用Task.Factory.ContinueWhenAny和Task.Factory.ContinueWhenAny<T>延续任务

我们可以等待多个任务,并在任何任务成功完成时链接延续代码:

```csharp
private static void ContinueWhenAny()
{
    int number = 13;
    Task<bool> taskA = Task.Factory.StartNew<bool>(() =>
        number / 2 != 0);
    Task<bool> taskB = Task.Factory.StartNew<bool>(() =>
        (number / 2) * 2 != number);
    Task<bool> taskC = Task.Factory.StartNew<bool>(() =>
        (number & 1) != 0);
    Task.Factory.ContinueWhenAny<bool>(new Task<bool>[]
    { taskA, taskB, taskC }, (task) =>
    {
        Console.WriteLine((task as Task<bool>).Result);
    }
    );
}
```

如前所示的代码中,我们有三种不同的逻辑来判断一个数字是否为奇数。假设我们不知道这些逻辑中哪一个会最快。为了计算结果,我们创建了三个任务,每个任务封装了不同的奇数查找逻辑,并并发运行它们。由于一个数字一次只能是奇数或偶数,所以所有线程的结果将是相同的,但它们的执行速度会有所不同。因此,获取第一个结果并丢弃其余结果是有意义的。这就是我们使用ContinueWhenAny方法实现的功能。

Parent and child tasks
Another type of relationship that can occur between threads is a parent-child relationship.
The child task is created as a nested task inside the body of the parent task. The child task
can be created either as attached or detached. Both types of tasks are created inside the
parent task and, by default, the created tasks are detached. We can make an attached task
by setting the AttachedToParent property of the task to true . You may want to consider
creating an attached task in any of the following scenarios:
All the exceptions that are thrown in the child task need to be propagated to the
parent
The status of the parent task is dependent on the child task
The parent needs to wait for the child task to finish

父子任务
线程之间可能发生的另一种关系是父子关系。
子任务作为父任务体内的嵌套任务创建。子任务可以创建为附加(attached)或分离(detached)。这两种类型的任务都在父任务内部创建,默认情况下,创建的任务都是分离的。我们可以通过将任务的AttachedToParent属性设置为true来创建一个附加任务。在以下任何情况中,您可能需要考虑创建一个附加任务:
所有在子任务中抛出的异常需要传递到父任务
父任务的状态依赖于子任务
父任务需要等待子任务完成

Creating a detached task
The code to create a detached class is as follows:
Task parentTask = Task. Factory. StartNew(() =>
{
Console. WriteLine(" Parent task started") ;
Task childTask = Task. Factory. StartNew(() => {
Console. WriteLine(" Child task started") ;
}) ;
Console. WriteLine(" Parent task Finish") ;
}) ;
//Wait for parent to finish
parentTask. Wait() ;
Console. WriteLine("Work Finished") ;
As you can see, we have created another task within the body of a task. By default, the child
or nested task is created as detached. We waited for the parent task to finish by calling
parentTask. Wait() . In the following output, you can see that the parent task doesn't wait
for the child task to finish and finishes first, followed by the child task starting:

创建一个分离任务
创建分离任务的代码如下:
```csharp
Task parentTask = Task.Factory.StartNew(() =>
{
    Console.WriteLine("父任务开始");
    Task childTask = Task.Factory.StartNew(() => {
        Console.WriteLine("子任务开始");
    });
    Console.WriteLine("父任务结束");
});
//等待父任务完成
parentTask.Wait();
Console.WriteLine("工作完成");
```
如你所见,我们在一个任务体内创建了另一个任务。默认情况下,子任务或嵌套任务是作为分离任务创建的。我们通过调用`parentTask.Wait()`等待父任务完成。在下面的输出中,你可以看到父任务并不等待子任务完成,而是先完成,然后子任务才开始:

Creating an attached task
An attached task is created similarly to a detached one. The only difference is that we set
the AttachedParent property of the task to true . This is demonstrated in the following
snippet:
Task parentTask = Task. Factory. StartNew(() =>
{
Console. WriteLine("Parent task started") ;
Task childTask = Task. Factory. StartNew(() => {
Console. WriteLine("Child task started") ;
}, TaskCreationOptions. AttachedToParent) ;
Console. WriteLine("Parent task Finish") ;
}) ;
//Wait for parent to finish
parentTask. Wait() ;
Console. WriteLine("Work Finished") ;
The output is as follows:
Here, you can see that the parent task does not finish until the child task has finished
executing.
In this section, we discussed advanced aspects of tasks, including creating relationships
among tasks. In the next section, we will dig more into working internally on tasks by
understanding the concept of work queues and how tasks deal with them.

创建一个附加任务
附加任务的创建方式与分离任务类似。唯一的区别是我们设置任务的`AttachedToParent`属性为`true`。以下代码片段展示了这一点:
```csharp
Task parentTask = Task.Factory.StartNew(() =>
{
    Console.WriteLine("父任务开始");
    Task childTask = Task.Factory.StartNew(() => {
        Console.WriteLine("子任务开始");
    }, TaskCreationOptions.AttachedToParent);
    Console.WriteLine("父任务结束");
});
//等待父任务完成
parentTask.Wait();
Console.WriteLine("工作完成");
```
输出如下:
这里,你可以看到父任务在子任务执行完毕之前不会结束。
在本节中,我们讨论了任务的高级方面,包括创建任务之间的关系。在下一节中,我们将更深入地探讨任务内部的工作,通过理解工作队列的概念以及任务如何处理它们。

Work-stealing queues
Work-stealing is a performance optimization technique for a thread pool. Every thread pool
maintains a single global queue of tasks that are created inside a process. In Chapter 1 ,
Introduction to Parallel Programming, we learned that the thread pool maintains an optimal
number of worker threads to work on tasks. The ThreadPool also maintains a thread
global queue, where it queues all the work items before they can be assigned to available
threads. Since this is a single queue and we work in multithreaded scenarios, we need to
implement thread-safety using synchronization primitives. With a single global queue,
synchronization leads to performance loss.
The .NET Framework works around this performance loss by introducing the concept of
local queues, which are managed by threads. Each thread has access to a global queue and
also maintains its own thread-local queue to store work items in. Parent tasks can be
scheduled inside the global queue. When tasks execute and need to create subtasks, they
can be stacked up on local queues and processed using the FIFO algorithm as soon as the
thread finishes executing.
The following diagram depicts the relationship between a global queue, a local queue, the
thread, and the Threadpool :
Let's say that the main thread creates a set of tasks. All of these tasks are queued to the
global queue to be executed later based on the availability of the thread inside the thread
pool. The following diagram depicts the global queue with all the queued tasks:
Let's say Task 1 is scheduled on Thread 1, Task 2 on Thread 2, and so on, as shown in the
following diagram:
If Task 1 and Task 2 generate more tasks, the new tasks will be stored in the thread-local
queue, as shown in the following diagram:
Similarly, if more tasks are created by these child tasks, they will go inside the local queue
instead of the global queue. Once Thread 1 has finished with Task 1, it will look into its
local queues and pick up the last task (LIFO). There is a high chance that the last task may
still be in the cache and so it doesn't need to be reloaded. Again, this improves
performance.
Once a thread (T1) exhausts its local queue, it will search in the global queue. If there are no
items in the global queue, it will search in the local queues for other threads (say T2). This
technique is called work-stealing and is an optimization technique. This time, it doesn't
pick the last task (LIFO) from T2 since the last item may still be in the T2 thread's cache.
Instead, it picks up the first task (FIFO) since there is a high chance that the thread has
moved out of T2's cache. This technique improves performance by making cached tasks
available to the local thread and out-of-cache tasks to other threads.

工作窃取队列
工作窃取是线程池的性能优化技术。每个线程池都维护着一个全局任务队列,这些任务在一个进程内部创建。在第1章《并行编程导论》中,我们了解到线程池维护了一定数量的工作线程来处理任务。ThreadPool还维护了一个线程全局队列,在任务可以被分配给可用线程之前,所有的工作项都在这个队列中排队。由于这是一个单一队列,我们在多线程场景下工作时,需要使用同步原语来实现线程安全。有了单一全局队列,同步会导致性能损失。
.NET Framework通过引入局部队列的概念来解决这种性能损失,这些局部队列由线程管理。每个线程都可以访问全局队列,并且还维护自己的线程本地队列来存储工作项。父任务可以在全局队列中被调度。当任务执行并需要创建子任务时,它们可以堆叠在局部队列上,并在线程完成执行后立即使用FIFO算法进行处理。
下面的图表展示了全局队列、局部队列、线程和线程池之间的关系:


假设主线程创建了一系列任务。所有这些任务都被排队到全局队列中,以便稍后根据线程池中的线程可用性执行。下面的图表展示了带有所有排队任务的全局队列:


假设任务1被安排在线程1上执行,任务2在线程2上执行,等等,如下图表所示:


如果任务1和任务2生成了更多任务,新任务将被存储在线程本地队列中,如下图表所示:


同样,如果这些子任务创建了更多任务,它们将进入局部队列而不是全局队列。一旦线程1完成了任务1,它将查看其局部队列并取出最后一个任务(LIFO)。最后一个任务很有可能仍在缓存中,因此不需要重新加载。这再次提高了性能。
一旦线程(T1)耗尽了它的局部队列,它将在全局队列中搜索。如果全局队列中没有项目,它将在其他线程(比如T2)的局部队列中搜索。这种技术称为工作窃取,是一种优化技术。这次,它不会从T2中取出最后一个任务(LIFO),因为最后一个项目可能仍在T2线程的缓存中。相反,它取出第一个任务(FIFO),因为很有可能该线程已经不在T2的缓存中了。这种技术通过使缓存任务对本地线程可用,而将不在缓存的任务提供给其他线程,从而提高了性能。

Summary
In this chapter, we have discussed how to break up tasks into smaller units so that each unit
can be handled independently by a thread. We have also learned about various ways we
can create tasks by utilizing ThreadPool . We introduced various techniques related to the
internal workings of tasks, including the concepts of work-stealing and task creation or
cancellation. We will be utilizing the knowledge we gained in this chapter in the rest of this
book.
In the next chapter, we will introduce the concepts of data parallelism. This will include
working with parallel loops and handling exceptions in them.

摘要
在本章中,我们讨论了如何将任务分解为更小的单元,以便每个单元可以由一个线程独立处理。我们还学习了利用线程池创建任务的各种方法。我们介绍了与任务内部工作原理相关的各种技术,包括工作窃取和任务创建或取消的概念。我们将在本书的其余部分利用在本章中获得的知识。
在下一章中,我们将介绍数据并行性的概念。这将包括使用并行循环以及在其中处理异常。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值