作者:[美]易格恩.阿格佛温(Eugene Agafonov)
译者:黄博文 黄辉兰
改编:陈广
日期:2018-3-19
总算走到这了,最新并行编程使用的都是Task,前面的知识可以蜻蜓点水,从这篇文章开始就要认真学了。
我们在之前的章节中学习了什么是线程,如何使用线程,以及为什么需要线程池。使用线程池可以使我们在减少并行度花销时节省操作系统资源。我们可以认为线程池是一个抽象层,其向程序员隐藏了使用线程的细节,使我们专心处理程序逻辑,而不是各种线程问题。
然而使用线程池也相当复杂。从线程池的工作线程中获取结果并不容易。我们需要实现自定义方式来获取结果,而且万一有异常发生,还需将异常正确地传播到初始线程中。除此以外,创建一组相关的异步操作,以及实现当前操作执行完成后下一操作才会执行的逻辑也不容易。
在尝试解决这些问题的过程中,创建了异步编程模型及基于事件的异步模式。之前提到过基于事件的异步模式。这些模式使得获取结果更容易,传播异常也更轻松,但是组合多个异步操作仍需大量工作,需要编写大量的代码。
为了解决所有问题,.NET Framework4.0引入了一个新的关于异步操作的API。它叫做**任务并行库(Task Parallel library,简称TPL)。.Net framework 4.5版对该API进行了轻微的改进,使用更简单。在本文的项目中将使用最新版的TPL,即.Net Framework 4.5版中的API。TPL可被认为是线程池之上的又一个抽象层,其对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度API。
TPL的核心概念是任务。一个任务代表了一个异步操作,该操作可以通过多种方式运行,可以使用或不使用独立线程运行。在本章中将探究任务的所有使用细节。
提示:
默认情况下,程序员无须知道任务实际上是如何执行的。TPL通过向用户隐藏任务的实现细节从而创建一个抽象层。遗憾的是,有些情况下这会导致诡秘的错误,比如试图获取任务的结果时程序被挂起。本章有助于理解TPL底层的原理,以及如何避免不恰当的使用方式。
一个任务可以通过多种方式和其他任务组合起来。例如,可以同时启动多个任务,等待所有任务完成,然后运行一个任务对之前所有任务的结果进行一些计算。TPL与之前的模式相比,其中一个关键优势是其具有用于组合任务的便利的API。
处理任务中的异常结果有多种方式。由于一个任务可能会由多个其他任务组成,这些任务也可能依次拥有各自的子任务,所以有一个AggregateException的概念。这种异常可以捕获底层任务内部的所有异常,并允许单独处理这些异常。
而且,C# 5.0已经内置了对TPL的支持,允许我们使用新的await和async关键字以平滑的、舒服的方式操作任务。在之后的文章会讨论该主题。
在本章中我们将学习使用TPL来执行异步操作。我们将学习什么是任务,如何用不同的方式创建任务,以及如何将任务组合在一起。我们会讨论如何将遗留的APM和EAP模式转换为使用任务,还有如何正确地处理异常,如何取消任务,以及如何使用多个任务同时执行。另外,还将讲述如何在Windows GUI应用程序中正确地使用任务。
运行以下代码:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.ComponentModel;
namespace Sync
{
class Program
{
static void Main(string[] args)
{
var t1 = new Task(() => TaskMethod("Task 1"));
var t2 = new Task(() => TaskMethod("Task 2"));
t2.Start();
t1.Start();
Task.Run(() => TaskMethod("Task 3"));
Task.Factory.StartNew(() => TaskMethod("Task 4"));
Task.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning);
Thread.Sleep(TimeSpan.FromSeconds(1));
}
static void TaskMethod(string name)
{
Console.WriteLine("Task {0} 正在运行, 线程id:{1},是否线程池线程: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
}
}
运行结果:
Task Task 3 正在运行, 线程id:6,是否线程池线程: True
Task Task 4 正在运行, 线程id:5,是否线程池线程: True
Task Task 2 正在运行, 线程id:3,是否线程池线程: True
Task Task 5 正在运行, 线程id:7,是否线程池线程: False
Task Task 1 正在运行, 线程id:4,是否线程池线程: True
当程序运行时,我们使用Task的构造函数创建了两个任务。我们传入一个lambda表达式作为Action委托。这可以使我们给TaskMethod
提供一个string参数。然后使用Start
方法运行这些任务。
请注意只有调用了这些任务的
Start
方法,才会执行任务。很容易忘记真正启动任务。
然后使用Task.Run
和Task.Factory.StartNew
方法来运行来运行另外两个任务。与使用Task构造函数的不同之处在于这两个被创建的任务会立即开始工作,所以无需显式地调用这些任务的Start
方法。从Task 1到Task 4的所有任务都被放置在线程池的工作线程中并以未指定的顺序运行。如果多次运行该程序,就会发现任务的执行顺序是不确定的。
Task.Run
方法只是Task.Factory.StartNew
的一个快捷方式,但是后者有附加的选项。通常如果无特殊需求,则可使用前一个方法,如 Task 5 所示。我们标记该任务为长时间运行,结果该任务将不会使用线程池,而在单独的线程中运行。然而,根据运行该任务的当前的任务调度程序(task scheduler),运行方式有可能不同。稍后会讲解什么是任务调试程序。
本节将描述如何从任务中获取结果值。我们将通过几个场景来了解在线程池中和主线程中运行任务的不同之处。
运行以下程序:
static void Main(string[] args)
{
TaskMethod("主线程Task");
Task<int> task = CreateTask("Task 1");
task.Start();
int result = task.Result;//这一句会导致程序阻塞
Console.WriteLine("结果为: {0}", result);
task = CreateTask("Task 2");
task.RunSynchronously();
result = task.Result;
Console.WriteLine("结果为: {0}", result);
task = CreateTask("Task 3");
Console.WriteLine(task.Status);
task.Start();
while (!task.IsCompleted)
{
Console.WriteLine(task.Status);
Thread.Sleep(500);
}
Console.WriteLine(task.Status);
result = task.Result;
Console.WriteLine("结果为: {0}", result);
}
static Task<int> CreateTask(string name)
{
return new Task<int>(() => TaskMethod(name));
}
static int TaskMethod(string name)
{
Console.WriteLine("{0} 正在运行,线程 id {1},是否线程池线程: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(2));
return 42;
}
运行结果:
主线程Task 正在运行,线程 id 1,是否线程池线程: False
Task 1 正在运行,线程 id 3,是否线程池线程: True
结果为: 42
Task 2 正在运行,线程 id 1,是否线程池线程: False
结果为: 42
Created
Task 3 正在运行,线程 id 3,是否线程池线程: True
Running
Running
Running
Running
RanToCompletion
结果为: 42
首先直接运行TaskMethod
方法,这是在主线程中执行,肯定没线程池什么事。id为1的线程就是主线程。
然后我们运行了Task 1
,使用Start
方法启动该任务并等待结果。该任务会被放置在线程池中。需要注意的是调用task.Result
会导致主线程等待,直到任务返回前一直处于阻塞状态。
Task 2
和Task 1
类似,除了Task 2
是通过RunSynchronously()
方法运行的。该任务会运行在主线程中,该任务的输出与第一个例子中直接同步调用TaskMethod
的输出完全一样。这是个非常好的优化,可以避免使用线程池来执行非常短暂的操作。
我们用以运行Task 1
相同的方式来运行Task 3
。但这次在阻塞主线程前打印出任务状态。
本节将展示如何设置相互依赖的任务。我们将学习如何创建一个任务,使其在父任务完成后才被运行。另外,将探寻为非常短暂的任务节省线程开销的可能性。
运行如下代码:
static void Main(string[] args)
{
var firstTask = new Task<int>(() => TaskMethod("First Task", 3));
var secondTask = new Task<int>(() => TaskMethod("Second Task", 2));
//此后续任务在线程池中运行
firstTask.ContinueWith(
t => Console.WriteLine("第一个结果是 {0}. 线程 id {1}, 是否线程池线程: {2}",
t.Result, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread),
TaskContinuationOptions.OnlyOnRanToCompletion);
firstTask.Start();
secondTask.Start();
Thread.Sleep(TimeSpan.FromSeconds(4));
//此后续任务在主线程中运行
Task continuation = secondTask.ContinueWith(
t => Console.WriteLine("第二个结果是 {0}. 线程 id {1}, 是否线程池线程: {2}",
t.Result, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread),
TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously);
//这是后续任务的后续任务
continuation.GetAwaiter().OnCompleted(
() => Console.WriteLine("Continuation Task 已完成! 线程 id {0}, 是否线程池线程: {1}",
Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread));
Thread.Sleep(TimeSpan.FromSeconds(2));
Console.WriteLine();
firstTask = new Task<int>(() =>
{
//firstTask里嵌套的子任务
var innerTask = Task.Factory.StartNew(() => TaskMethod("Second Task", 5), TaskCreationOptions.AttachedToParent);
//子任务的后续任务
innerTask.ContinueWith(t => TaskMethod("Third Task", 2), TaskContinuationOptions.AttachedToParent);
//下面这句代码也会在线程池内完成
return TaskMethod("First Task", 2);
});
firstTask.Start();
while (!firstTask.IsCompleted)
{
Console.WriteLine(firstTask.Status);
Thread.Sleep(TimeSpan.FromSeconds(0.5));
}
Console.WriteLine(firstTask.Status);
Thread.Sleep(TimeSpan.FromSeconds(10));
}
static int TaskMethod(string name, int seconds)
{
Console.WriteLine("{0} 正在运行,线程id {1},是否线程池线程: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
return 42 * seconds;
}
运行结果:
Second Task 正在运行,线程id 4,是否线程池线程: True
First Task 正在运行,线程id 3,是否线程池线程: True
第一个结果是 126. 线程 id 5, 是否线程池线程: True
第二个结果是 84. 线程 id 1, 是否线程池线程: False
Continuation Task 已完成! 线程 id 3, 是否线程池线程: True
Running
Second Task 正在运行,线程id 5,是否线程池线程: True
First Task 正在运行,线程id 3,是否线程池线程: True
Running
Running
Running
WaitingForChildrenToComplete
WaitingForChildrenToComplete
WaitingForChildrenToComplete
WaitingForChildrenToComplete
WaitingForChildrenToComplete
WaitingForChildrenToComplete
Third Task 正在运行,线程id 3,是否线程池线程: True
WaitingForChildrenToComplete
WaitingForChildrenToComplete
WaitingForChildrenToComplete
WaitingForChildrenToComplete
RanToCompletion
这程序设计得真够复杂,就快晕倒了。
当主程序启动时,我们创建了两个任务,并为第一个任务设置了一个后续操作(continuation,一个代码块,会在当前任务完成后运行)。然后启动这两个任务并等待4秒,这个时间足够两个任务完成。然后给第二个任务运行另一个后续操作,并通过指定TaskContinuationOptions.ExecuteSynchronously
选项来尝试同步执行该后续操作。如果后续操作耗时非常短暂,使用以上方式是非常有用的,因为放置在主线程中运行比放置在线程池中运行要快。可以实现这一点是因为第二个任务恰好在那一刻完成。如果注释掉4秒的Thread.Sleep
方法,将会看到该代码放置到线程池中,这是因为还未从之前的任务中得到结果。
最后我们为之前的后续操作也定义了一个后续操作,但这里使用了一个稍微不同的方式,即使用了新的GetAwaiter
和OnCompleted
方法。这些方法是 C# 5.0 语言中异步机制中的方法。以后会讨论。
本节示例的最后部分与父子线程有关。我们创建了一个新任务,当运行该任务时,通过提供一个TaskContinuationOptions.AttachedToParent
选项来运行一个所谓的子任务。
注意:子任务必须在父任务运行时创建,并正确的附加给父任务!
这意味着只有所有子任务结束工作,父任务才会完成。通过提供一个TaskContinuationOptions
选项也可以在子任务上运行后续操作。该后续操作也会影响父任务,并且直到最后一个子任务结束后它才会运行完成。
本节是关于如何给基于任务的异步操作实现取消流程。我们将学习如何正确的使用取消标志,以及在任务真正运行前如何得知其是否被取消。
运行以下代码:
private static void Main(string[] args)
{
var cts = new CancellationTokenSource();
var longTask = new Task<int>(() => TaskMethod("Task 1", 10, cts.Token), cts.Token);
Console.WriteLine(longTask.Status);
cts.Cancel(); //还未运行就被取消了
Console.WriteLine(longTask.Status);
Console.WriteLine("第一个 task 在运行前被取消");
cts = new CancellationTokenSource();
longTask = new Task<int>(() => TaskMethod("Task 2", 10, cts.Token), cts.Token);
longTask.Start();
for (int i = 0; i < 5; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(0.5));
Console.WriteLine(longTask.Status);
}
cts.Cancel(); //中途取消
for (int i = 0; i < 5; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(0.5));
Console.WriteLine(longTask.Status);
}
Console.WriteLine("task 已结束,返回结果 {0}.", longTask.Result);
}
private static int TaskMethod(string name, int seconds, CancellationToken token)
{
Console.WriteLine("Task {0} 正在运行,线程 id {1}. 是否线程池线程: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
for (int i = 0; i < seconds; i++)
{
Thread.Sleep(TimeSpan.FromSeconds(1));
if (token.IsCancellationRequested) return -1;
}
return 42 * seconds;
}
运行结果:
Created
Canceled
第一个 task 在运行前被取消
Task Task 2 正在运行,线程 id 3. 是否线程池线程: True
Running
Running
Running
Running
Running
RanToCompletion
RanToCompletion
RanToCompletion
RanToCompletion
RanToCompletion
task 已结束,返回结果 -1.
之前已经讨论过取消标志概念,你已经相当熟悉了。而本节又是一个关于为TPL任务实现取消选项的简单例子。
首先仔细看看longTask的创建代码。我们将给底层任务传递一次取消标志,然后给任务构造函数再传递一次。为什么需要提供取消标志两次呢?
答案是如果在任务实际启动前取消它,该任务的TPL基础设施有责任处理该取消操作,因为这些代码根本不会执行。通过得到的第一个任务的状态可以知道它被取消了。如果尝试对该任务调用Start方法,将会得到的第一个任务的状态可以知道它被取消了。如果尝试对该任务调用STart方法,将会得到InvalidOperationException异常。
然后需要自己写代码来处理取消过程。这意味着我们对取消过程全权负责,并且在取消任务后,任务的状态仍然是RanToCompletion,因为从TPL的视角来看,该任务正常完成了它的工作。辨别这两种情况是非常重要的,并且需要理解每种情况下职责的不同。
本节将描述异步任务中处理异常这一重要的主题。我们将讨论任务中抛出异常的不同情况,以及如何获取这些异常信息。
运行以下代码:
static void Main(string[] args)
{
Task<int> task;
try
{
task = Task.Run(() => TaskMethod("Task 1", 2));
int result = task.Result;
Console.WriteLine("结果: {0}", result);
}
catch (Exception ex)
{
Console.WriteLine("捕获了异常: {0}", ex);
}
Console.WriteLine("----------------------------------------------");
Console.WriteLine();
try
{
task = Task.Run(() => TaskMethod("Task 2", 2));
int result = task.GetAwaiter().GetResult();
Console.WriteLine("结果: {0}", result);
}
catch (Exception ex)
{
Console.WriteLine("捕获了异常: {0}", ex);
}
Console.WriteLine("----------------------------------------------");
Console.WriteLine();
var t1 = new Task<int>(() => TaskMethod("Task 3", 3));
var t2 = new Task<int>(() => TaskMethod("Task 4", 2));
var complexTask = Task.WhenAll(t1, t2);
var exceptionHandler = complexTask.ContinueWith(t =>
Console.WriteLine("捕获了异常: {0}", t.Exception),
TaskContinuationOptions.OnlyOnFaulted
);
t1.Start();
t2.Start();
Thread.Sleep(TimeSpan.FromSeconds(5));
Console.ReadLine();
}
static int TaskMethod(string name, int seconds)
{
Console.WriteLine("Task {0} 正在运行,线程 id {1}. 是否线程池线程: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
throw new Exception("Boom!");
return 42 * seconds;
}
本代码需要生成.exe
文件方能完整运行。请参考《线程同步》这篇文章中的【使用Mutex类】这一节来生成.exe
文件运行。
运行结果:
之前在《线程基础》这篇文章中的【处理异常】这一节中,我们知道,异常应当在工作线程中捕捉,而不是放到主线程中捕捉。但TPL中的工作线程会将异常封闭并传递给主线程捕获。
当程序启动时,创建了一个任务并尝试同步获取任务结果。Result
属性的Get
部分会使当前线程等待直到该任务完成,并将异常传播给当前线程。在这种情况下,通过catch代码块可以很容易地捕获异常,但是该异常是一个被封装的异常,叫做AggregateException
。在本例中,它里面包含一个异常,因为只有一个任务抛出了异常。可以访问InnerException
属性来得到底层异常。
第二个例子与第一个非常相似,不同之处是使用GetAwaiter
和GetResult
方法来访问任务情况。这种情况下无需封装异常,因为TPL基础设施会提取该异常。如果只有一个底层任务,那么一次只能获取一个原始异常,这种设计非常合适。
最后一个例子展示了两个任务抛出异常的情形。现在使用后续操作来处理异常。只有之前的任务完成前有异常时,该后续操作才会被执行。通过给后续操作传递TaskContinuationOptions.OnlyOnFaulted
选项可以实现该行为。结果打印出了AggregateException
,其内部封闭了两个任务抛出的异常。Task.WhenAll(t1, t2)
表示当t1
和t2
都执行完毕后才执行complexTask
。
由于任务可以以非常不同的方式连接,因此结果的AggregateException
异常可能包含他内部包含普通异常的聚合异常。这些内部的聚合异常自身也可包含其他的聚合异常。
为了摆脱这些对异常的封闭,欠可以使用根聚合异常的Flatten
方法。它将返回一个集合。该集合包含以层级结构中每个子聚合异常中的内部异常。
本节展示了如何同时运行多个异步任务。我们将学习当所有任务都完成或任意一个任务完成了工作时,如何高效地得到通知。
使用以下命名空间:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
添加如下代码:
static void Main(string[] args)
{
var firstTask = new Task<int>(() => TaskMethod("First Task", 3));
var secondTask = new Task<int>(() => TaskMethod("Second Task", 2));
var whenAllTask = Task.WhenAll(firstTask, secondTask);
whenAllTask.ContinueWith(t =>
Console.WriteLine("第一个任务结果为 {0}, 第二个任务结果为 {1}", t.Result[0], t.Result[1]),
TaskContinuationOptions.OnlyOnRanToCompletion
);
firstTask.Start();
secondTask.Start();
Thread.Sleep(TimeSpan.FromSeconds(4));
var tasks = new List<Task<int>>();
for (int i = 1; i < 4; i++)
{
int counter = i;
var task = new Task<int>(() => TaskMethod(string.Format("Task {0}", counter), counter));
tasks.Add(task);
task.Start();
}
while (tasks.Count > 0)
{
var completedTask = Task.WhenAny(tasks).Result;
tasks.Remove(completedTask);
Console.WriteLine("一个任务已完成,结果为 {0}.", completedTask.Result);
}
Thread.Sleep(TimeSpan.FromSeconds(1));
}
static int TaskMethod(string name, int seconds)
{
Console.WriteLine("{0} 正在运行,线程 id {1}. 是否线程池线程: {2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
Thread.Sleep(TimeSpan.FromSeconds(seconds));
return 42 * seconds;
}
运行结果:
Second Task 正在运行,线程 id 4. 是否线程池线程: True
First Task 正在运行,线程 id 3. 是否线程池线程: True
第一个任务结果为 126, 第二个任务结果为 84
Task 1 正在运行,线程 id 4. 是否线程池线程: True
Task 2 正在运行,线程 id 3. 是否线程池线程: True
Task 3 正在运行,线程 id 6. 是否线程池线程: True
一个任务已完成,结果为 42.
一个任务已完成,结果为 84.
一个任务已完成,结果为 126.
当程序启动时,创建了两个任务。然后借助于Task.WhenAll
方法,创建了第三个任务,该任务将会在所有任务完成后运行。该任务的结果提供了一个结果数组,第一个元素是第一个任务的结果,第二个元素是第二个任务的结果,以此类推。
然后我们创建了另外一系列任务,并使用Task.WhenAny
方法等待这些任务中的人任何一个完成。当有一个任务完成后,从列表中移除该任务并继续等待其他任务完成,直到列表为空。获取任务的完成进展情况或在运行任务时使用超时,都可以使用Task.WhenAny
方法。例如,我们等待一组任务运行,并且使用其中一个任务用来记录是否超时。如果该任务先完成,则只需取消掉其他还未完成的任务。
本节将描述处理任务的另一个重要方面,即通过异步代码与UI正确地交互。我们将学习什么是任务调度程序,为什么它如此重要,它如何损害应用程序,以及如何在使用避免错误。
此例需要在Visual Studio中使用Windows窗体应用程序编写代码。
打开Visual Studio,新建一个【Windows窗体应用】,在窗体中放一个【TextBox】控件,三个【Button】控件,如下图所示:
三个按钮分别命名为:btnSync
、btnAsync
、btnAsyncOK
,TextBox命名为ContentTextBlock
。
分别双击三个按钮生成事件方法,引入命名空间:System.Threading
。最终输入代码如下:
private void btnSync_Click(object sender, EventArgs e)
{
ContentTextBlock.Text = string.Empty;
try
{
//string result = TaskMethod(TaskScheduler.FromCurrentSynchronizationContext()).Result;
//同步运行方法
string result = TaskMethod().Result;
ContentTextBlock.Text = result;
}
catch (Exception ex)
{
ContentTextBlock.Text = ex.InnerException.Message;
}
}
private void btnAsync_Click(object sender, EventArgs e)
{
ContentTextBlock.Text = string.Empty;
this.Cursor = Cursors.WaitCursor;
//此处最终使用的是TaskScheduler.Default,使得Task在线程池中运行,无法访问UI
Task<string> task = TaskMethod();
task.ContinueWith(t => {
//此处使用的是TaskScheduler.FromCurrentSynchronizationContext
//使得Task在UI线程中运行,从而可以将异常打印到UI内
ContentTextBlock.Text = t.Exception.InnerException.Message;
this.Cursor = Cursors.Arrow;
},
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted,
TaskScheduler.FromCurrentSynchronizationContext());//此选项使得方法在UI线程中运行
}
private void btnAsyncOk_Click(object sender, EventArgs e)
{
ContentTextBlock.Text = string.Empty;
this.Cursor = Cursors.WaitCursor;
//以下task全部运行于UI线程,可以在UI打印结果
Task<string> task = TaskMethod(TaskScheduler.FromCurrentSynchronizationContext());
task.ContinueWith(t => this.Cursor = Cursors.Arrow,
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
Task<string> TaskMethod()
{
return TaskMethod(TaskScheduler.Default);
}
Task<string> TaskMethod(TaskScheduler scheduler)
{
Task delay = Task.Delay(TimeSpan.FromSeconds(5));
return delay.ContinueWith(t =>
{
string str = string.Format("Task 运行中,线程 id {0}. 是否线程池线程: {1}",
Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
ContentTextBlock.Text = str;
return str;
}, scheduler);
}
运行程序,结果如下图所示。蓝框表示运行时所点击的按钮:
TaskScheduler是一个非常重要的抽象。该组件实际上负责如何执行任务。默认的任务调度程序将任务放置到线程池找作线程中。这是非常见的场景,所以TPL将其作为默认选项并不奇怪。我们已经知道了如何同步运行任务,以及如何将任务附加到父任务上,从而一起运行。现在让我们看看使用任务的其他方式。
当程序启动时,创建了一个包含三个按钮的窗口。第一个按钮调用了一个同步任务的执行。该代码被放置在btnAsync_Click
方法中。当任务运行时,我们甚至无法移动应用程序窗口。当用户界面线程忙于运行任务时,整个用户界面被完全冻结,在任务完成前无法响应任何消息循环。对于GUI窗口程序来说这是一个相当不好的实践,我们需要找到一个方式来解决该问题。
第二个问题是我们尝试从其他线程访问UI控制器。图形用户界面控制器从没有被设计为可被多线程使用,并且为了避免可能的错误,不允许从创建UI线程之外的线程中访问UI组件。当我们尝试这样做时,得到了一个异常,该异常信息5秒后打印到了主窗口中。
为了解决第一个问题,我们尝试异步运行任务。第二个按钮就是这样做的。该代码被放置在btnAsync_Click
方法中。当使用调试模式运行该任务时,将会看到该任务被放置在线程池中,最后将得到同样的异常。然而,当任务运行时用户界面一直保持响应。这是好事,但是我们仍需要除掉异常。
给TaskScheduler.FromCurrentSynchronizationContext
选项提供一个后续操作用于输出错误信息。如果不这样做,我们将无法看到错误信息,因为可能会得到在任务中产生的相同异常。该选项驱使TPL基础设施给UI线程的后续操作中放入代码,并借助UI线程消息循环来异步运行该代码。这解决了从其他线程访问UI控制器并仍保持UI处于响应状态的问题。
为了检查是否真的是这样,可以按下最后一个按钮来运行btnAsyncOk_Click
方法中的代码。与其余例子不同之处在于我们将UI线程任务调度程序提供给了该任务。你将看到任务以异步方式运行在UI线程中。UI依然保持响应。甚至尽管等待光标处于激活状态,你仍可以按下另一个按钮。
然而使用UI线程运行任务有一些技巧。如果回到同步任务代码,取消对使用UI线程任务调度程序获取结果的代码行的注释,我们将永远得不到任何结果。这是一个经典的死锁情况:我们在UI线程队列中调度了一个操作,UI线程等待该操作完成,但当等待时,它又无法运行该操作,这将永不会结束(甚至永不会开始)。如果在任务中调用Wait
方法也会发生死锁。为了避免死锁,绝对不要通过任务调度程序在UI线程中使用同步操作,请使用C# 5.0中的ContinueWith
或async/await
方法。