C#多线程编程系列(五)- 使用任务并行库

目录

  • 1.1 简介
  • 1.2 创建任务
  • 1.3 使用任务执行基本的操作
  • 1.4 组合任务
  • 1.5 将APM模式转换为任务
  • 1.6 将EAP模式转换为任务
  • 1.7 实现取消选项
  • 1.8 处理任务中的异常
  • 1.9 并行运行任务
  • 1.10 使用TaskScheduler配置任务执行
  • 参考书籍
  • 笔者水平有限,如果错误欢迎各位批评指正!

本系列首页链接:[C#多线程编程系列(一)- 简介 ]


1.1 简介#

在之前的几个章节中,就线程的使用和多线程相关的内容进行了介绍。因为线程涉及到异步、同步、异常传递等问题,所以在项目中使用多线程的代价是比较高昂的,需要编写大量的代码来达到正确性和健壮性。

为了解决这样一些的问题,在.Net Framework 4.0中引入了一个关于一步操作的API。它叫做任务并行库(Task Parallel Library)。然后在.Net Framwork 4.5中对它进行了轻微的改进,本文的案例都是用最新版本的TPL库,而且我们还可以使用C# 5.0的新特性await/async来简化TAP编程,当然这是之后才介绍的。

TPL内部使用了线程池,但是效率更高。在把线程归还回线程池之前,它会在同一线程中顺序执行多少Task,这样避免了一些小任务上下文切换浪费时间片的问题。

任务是对象,其中封装了以异步方式执行的工作,但是委托也是封装了代码的对象。任务和委托的区别在于,委托是同步的,而任务是异步的。

在本章中,我们将会讨论如何使用TPL库来进行任务之间的组合同步,如何将遗留的APM和EAP模式转换为TPL模式等等。

1.2 创建任务#

在本节中,主要是演示了如何创建一个任务。其主要用到了System.Threading.Tasks命名空间下的Task类。该类可以被实例化并且提供了一组静态方法,可以方便快捷的创建任务。

在下面实例代码中,分别延时了三种常见的任务创建方式,并且创建任务是可以指定任务创建的选项,从而达到最优的创建方式。

TaskCreationOptions中一共有7个枚举,枚举是可以使用|运算符组合定义的。其枚举如下表所示。

成员名称说明
AttachedToParent指定将任务附加到任务层次结构中的某个父级。 默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行。 可以使用 TaskContinuationOptions.AttachedToParent 选项以便将父任务和子任务同步。请注意,如果使用 DenyChildAttach 选项配置父任务,则子任务中的 AttachedToParent 选项不起作用,并且子任务将作为分离的子任务执行。有关详细信息,请参阅附加和分离的子任务。
DenyChildAttach指定任何尝试作为附加的子任务执行(即,使用 AttachedToParent 选项创建)的子任务都无法附加到父任务,会改成作为分离的子任务执行。 有关详细信息,请参阅附加和分离的子任务。
HideScheduler防止环境计划程序被视为已创建任务的当前计划程序。 这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为 Default 当前计划程序。
LongRunning指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。 它会向 TaskScheduler 提示,过度订阅可能是合理的。 可以通过过度订阅创建比可用硬件线程数更多的线程。 它还将提示任务计划程序:该任务需要附加线程,以使任务不阻塞本地线程池队列中其他线程或工作项的向前推动。
None指定应使用默认行为。
PreferFairness提示 TaskScheduler 以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。
RunContinuationsAsynchronously强制异步执行添加到当前任务的延续任务。请注意,RunContinuationsAsynchronously 成员在以 .NET Framework 4.6 开头的 TaskCreationOptions 枚举中可用。
 

Copy

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 方法启动任务 不需要手动启动 Task.Run(() => TaskMethod("Task 3")); // 使用 Task.Factory.StartNew方法 启动任务 实际上就是Task.Run Task.Factory.StartNew(() => TaskMethod("Task 4")); // 在StartNew的基础上 添加 TaskCreationOptions.LongRunning 告诉 Factory该任务需要长时间运行 // 那么它就会可能会创建一个 非线程池线程来执行任务 Task.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning); ReadLine(); } static void TaskMethod(string name) { WriteLine($"任务 {name} 运行,线程 id {CurrentThread.ManagedThreadId}. 是否为线程池线程: {CurrentThread.IsThreadPoolThread}."); }

运行结果如下图所示。

1533608520548

1.3 使用任务执行基本的操作#

在本节中,使用任务执行基本的操作,并且获取任务执行完成后的结果值。本节内容比较简单,在此不做过多介绍。

演示代码如下,在主线程中要获取结果值,常用的方式就是访问task.Result属性,如果任务线程还没执行完毕,那么会阻塞主线程,直到线程执行完。如果任务线程执行完毕,那么将直接拿到运算的结果值。

Task 3中,使用了task.Status来打印线程的状态,线程每个状态的具体含义,将在下一节中介绍。

 

Copy

static void Main(string[] args) {
// 直接执行方法 作为参照 TaskMethod("主线程任务");
// 访问 Result属性 达到运行结果
 Task<int> task = CreateTask("Task 1");
 task.Start();
int result = task.Result;
WriteLine($"运算结果: {result}");
// 使用当前线程,同步执行任务
task = CreateTask("Task 2");
task.RunSynchronously();
result = task.Result;
WriteLine($"运算结果:{result}");
// 通过循环等待 获取运行结果
task = CreateTask("Task 3");
WriteLine(task.Status);
task.Start();
while (!task.IsCompleted) {
WriteLine(task.Status);
Sleep(TimeSpan.FromSeconds(0.5));
}
WriteLine(task.Status);
result = task.Result;
WriteLine($"运算结果:{result}");
Console.ReadLine();
}
static Task<int> CreateTask(string name) {
return new Task<int>(() => TaskMethod(name));
}
static int TaskMethod(string name) {
WriteLine($"{name} 运行在线程 {CurrentThread.ManagedThreadId}上. 是否为线程池线程{CurrentThread.IsThreadPoolThread}");
Sleep(TimeSpan.FromSeconds(2));
return 42;
}

运行结果如下,可见Task 1 和Task 2均是运行在主线程上,并非线程池线程。

1533798340309

1.4 组合任务#

在本节中,体现了任务其中一个强大的功能,那就是组合任务。通过组合任务可很好的描述任务与任务之间的异步、同步关系,大大降低了编程的难度。

组合任务主要是通过task.ContinueWith()task.WhenAny()task.WhenAll()等和task.GetAwaiter().OnCompleted()方法来实现。

在使用task.ContinueWith()方法时,需要注意它也可传递一系列的枚举选项TaskContinuationOptions,该枚举选项和TaskCreationOptions类似,其具体定义如下表所示。

成员名称说明
AttachedToParent如果延续为子任务,则指定将延续附加到任务层次结构中的父级。 只有当延续前面的任务也是子任务时,延续才可以是子任务。 默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行。 可以使用 TaskContinuationOptions.AttachedToParent 选项以便将父任务和子任务同步。请注意,如果使用 DenyChildAttach 选项配置父任务,则子任务中的 AttachedToParent 选项不起作用,并且子任务将作为分离的子任务执行。有关更多信息,请参见Attached and Detached Child Tasks。
DenyChildAttach指定任何使用 TaskCreationOptions.AttachedToParent 选项创建,并尝试作为附加的子任务执行的子任务(即,由此延续创建的任何嵌套内部任务)都无法附加到父任务,会改成作为分离的子任务执行。 有关详细信息,请参阅附加和分离的子任务。
ExecuteSynchronously指定应同步执行延续任务。 指定此选项后,延续任务在导致前面的任务转换为其最终状态的相同线程上运行。如果在创建延续任务时已经完成前面的任务,则延续任务将在创建此延续任务的线程上运行。 如果前面任务的 CancellationTokenSource 已在一个 finally(在 Visual Basic 中为 Finally)块中释放,则使用此选项的延续任务将在该 finally 块中运行。 只应同步执行运行时间非常短的延续任务。由于任务以同步方式执行,因此无需调用诸如 Task.Wait 的方法来确保调用线程等待任务完成。
HideScheduler指定由延续通过调用方法(如 Task.Run 或 Task.ContinueWith)创建的任务将默认计划程序 (TaskScheduler.Default) 视为当前的计划程序,而不是正在运行该延续的计划程序。
LazyCancellation在延续取消的情况下,防止延续的完成直到完成先前的任务。
LongRunning指定延续将是长期运行的、粗粒度的操作。 它会向 TaskScheduler 提示,过度订阅可能是合理的。
None如果未指定延续选项,应在执行延续任务时使用指定的默认行为。 延续任务在前面的任务完成后以异步方式运行,与前面任务最终的 Task.Status 属性值无关。 如果延续为子任务,则会将其创建为分离的嵌套任务。
NotOnCanceled指定不应在延续任务前面的任务已取消的情况下安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.Canceled,则前面的任务会取消。 此选项对多任务延续无效。
NotOnFaulted指定不应在延续任务前面的任务引发了未处理异常的情况下安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.Faulted,则前面的任务会引发未处理的异常。 此选项对多任务延续无效。
NotOnRanToCompletion指定不应在延续任务前面的任务已完成运行的情况下安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.RanToCompletion,则前面的任务会运行直至完成。 此选项对多任务延续无效。
OnlyOnCanceled指定只应在延续前面的任务已取消的情况下安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.Canceled,则前面的任务会取消。 此选项对多任务延续无效。
OnlyOnFaulted指定只有在延续任务前面的任务引发了未处理异常的情况下才应安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.Faulted,则前面的任务会引发未处理的异常。OnlyOnFaulted 选项可保证前面任务中的 Task.Exception 属性不是 null。 你可以使用该属性来捕获异常,并确定导致任务出错的异常。 如果你不访问 Exception 属性,则不会处理异常。 此外,如果尝试访问已取消或出错的任务的 Result 属性,则会引发一个新异常。此选项对多任务延续无效。
OnlyOnRanToCompletion指定只应在延续任务前面的任务已完成运行的情况下才安排延续任务。 如果前面任务完成的 Task.Status 属性是 TaskStatus.RanToCompletion,则前面的任务会运行直至完成。 此选项对多任务延续无效。
PreferFairness提示 TaskScheduler 按任务计划的顺序安排任务,因此较早安排的任务将更可能较早运行,而较晚安排运行的任务将更可能较晚运行。
RunContinuationsAsynchronously指定应异步运行延续任务。 此选项优先于 TaskContinuationOptions.ExecuteSynchronously。

演示代码如下所示,使用ContinueWith()OnCompleted()方法组合了任务来运行,搭配不同的TaskCreationOptionsTaskContinuationOptions来实现不同的效果。

 

Copy

static void Main(string[] args) { WriteLine($"主线程 线程 Id {CurrentThread.ManagedThreadId}"); // 创建两个任务 var firstTask = new Task<int>(() => TaskMethod("Frist Task",3)); var secondTask = new Task<int>(()=> TaskMethod("Second Task",2)); // 在默认的情况下 ContiueWith会在前面任务运行后再运行 firstTask.ContinueWith(t => WriteLine($"第一次运行答案是 {t.Result}. 线程Id {CurrentThread.ManagedThreadId}. 是否为线程池线程: {CurrentThread.IsThreadPoolThread}")); // 启动任务 firstTask.Start(); secondTask.Start(); Sleep(TimeSpan.FromSeconds(4)); // 这里会紧接着 Second Task运行后运行, 但是由于添加了 OnlyOnRanToCompletion 和 ExecuteSynchronously 所以会由运行SecondTask的线程来 运行这个任务 Task continuation = secondTask.ContinueWith(t => WriteLine($"第二次运行的答案是 {t.Result}. 线程Id {CurrentThread.ManagedThreadId}. 是否为线程池线程:{CurrentThread.IsThreadPoolThread}"),TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously); // OnCompleted 是一个事件 当contiuation运行完成后 执行OnCompleted Action事件 continuation.GetAwaiter().OnCompleted(() => WriteLine($"后继任务完成. 线程Id {CurrentThread.ManagedThreadId}. 是否为线程池线程 {CurrentThread.IsThreadPoolThread}")); Sleep(TimeSpan.FromSeconds(2)); WriteLine(); firstTask = new Task<int>(() => { // 使用了TaskCreationOptions.AttachedToParent 将这个Task和父Task关联, 当这个Task没有结束时 父Task 状态为 WaitingForChildrenToComplete var innerTask = Task.Factory.StartNew(() => TaskMethod("Second Task",5), TaskCreationOptions.AttachedToParent); innerTask.ContinueWith(t => TaskMethod("Thrid Task", 2), TaskContinuationOptions.AttachedToParent); return TaskMethod("First Task",2); }); firstTask.Start(); // 检查firstTask线程状态 根据上面的分析 首先是 Running -> WatingForChildrenToComplete -> RanToCompletion while (! firstTask.IsCompleted) { WriteLine(firstTask.Status); Sleep(TimeSpan.FromSeconds(0.5)); } WriteLine(firstTask.Status); Console.ReadLine(); } static int TaskMethod(string name, int seconds) { WriteLine($"任务 {name} 正在运行,线程池线程 Id {CurrentThread.ManagedThreadId},是否为线程池线程: {CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(seconds)); return 42 * seconds; }

运行结果如下图所示,与预期结果一致。其中使用了task.Status来打印任务运行的状态,对于task.Status的状态具体含义如下表所示。

成员名称说明
Canceled该任务已通过对其自身的 CancellationToken 引发 OperationCanceledException 对取消进行了确认,此时该标记处于已发送信号状态;或者在该任务开始执行之前,已向该任务的 CancellationToken 发出了信号。 有关详细信息,请参阅任务取消。
Created该任务已初始化,但尚未被计划。
Faulted由于未处理异常的原因而完成的任务。
RanToCompletion已成功完成执行的任务。
Running该任务正在运行,但尚未完成。
WaitingForActivation该任务正在等待 .NET Framework 基础结构在内部将其激活并进行计划。
WaitingForChildrenToComplete该任务已完成执行,正在隐式等待附加的子任务完成。
WaitingToRun该任务已被计划执行,但尚未开始执行。

1533798776604

1.5 将APM模式转换为任务#

在前面的章节中,介绍了基于IAsyncResult接口实现了BeginXXXX/EndXXXX方法的就叫APM模式。APM模式非常古老,那么如何将它转换为TAP模式呢?对于常见的几种APM模式异步任务,我们一般选择使用Task.Factory.FromAsync()方法来实现将APM模式转换为TAP模式

演示代码如下所示,比较简单不作过多介绍。

 

Copy

static void Main(string[] args) { int threadId; AsynchronousTask d = Test; IncompatibleAsychronousTask e = Test; // 使用 Task.Factory.FromAsync方法 转换为Task WriteLine("Option 1"); Task<string> task = Task<string>.Factory.FromAsync(d.BeginInvoke("异步任务线程", CallBack, "委托异步调用"), d.EndInvoke); task.ContinueWith(t => WriteLine($"回调函数执行完毕,现在运行续接函数!结果:{t.Result}")); while (!task.IsCompleted) { WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(0.5)); } WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(1)); WriteLine("----------------------------------------------"); WriteLine(); // 使用 Task.Factory.FromAsync重载方法 转换为Task WriteLine("Option 2"); task = Task<string>.Factory.FromAsync(d.BeginInvoke,d.EndInvoke,"异步任务线程","委托异步调用"); task.ContinueWith(t => WriteLine($"任务完成,现在运行续接函数!结果:{t.Result}")); while (!task.IsCompleted) { WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(0.5)); } WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(1)); WriteLine("----------------------------------------------"); WriteLine(); // 同样可以使用 FromAsync方法 将 BeginInvoke 转换为 IAsyncResult 最后转换为 Task WriteLine("Option 3"); IAsyncResult ar = e.BeginInvoke(out threadId, CallBack, "委托异步调用"); task = Task<string>.Factory.FromAsync(ar, _ => e.EndInvoke(out threadId, ar)); task.ContinueWith(t => WriteLine($"任务完成,现在运行续接函数!结果:{t.Result},线程Id {threadId}")); while (!task.IsCompleted) { WriteLine(task.Status); Sleep(TimeSpan.FromSeconds(0.5)); } WriteLine(task.Status); ReadLine(); } delegate string AsynchronousTask(string threadName); delegate string IncompatibleAsychronousTask(out int threadId); static void CallBack(IAsyncResult ar) { WriteLine("开始运行回调函数..."); WriteLine($"传递给回调函数的状态{ar.AsyncState}"); WriteLine($"是否为线程池线程:{CurrentThread.IsThreadPoolThread}"); WriteLine($"线程池工作线程Id:{CurrentThread.ManagedThreadId}"); } static string Test(string threadName) { WriteLine("开始运行..."); WriteLine($"是否为线程池线程:{CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(2)); CurrentThread.Name = threadName; return $"线程名:{CurrentThread.Name}"; } static string Test(out int threadId) { WriteLine("开始运行..."); WriteLine($"是否为线程池线程:{CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(2)); threadId = CurrentThread.ManagedThreadId; return $"线程池线程工作Id是:{threadId}"; }

运行结果如下图所示。

1533778462479

1.6 将EAP模式转换为任务#

在上几章中有提到,通过BackgroundWorker类通过事件的方式实现的异步,我们叫它EAP模式。那么如何将EAP模式转换为任务呢?很简单,我们只需要通过TaskCompletionSource类,即可将EAP模式转换为任务。

演示代码如下所示。

 

Copy

static void Main(string[] args) { var tcs = new TaskCompletionSource<int>(); var worker = new BackgroundWorker(); worker.DoWork += (sender, eventArgs) => { eventArgs.Result = TaskMethod("后台工作", 5); }; // 通过此方法 将EAP模式转换为 任务 worker.RunWorkerCompleted += (sender, eventArgs) => { if (eventArgs.Error != null) { tcs.SetException(eventArgs.Error); } else if (eventArgs.Cancelled) { tcs.SetCanceled(); } else { tcs.SetResult((int)eventArgs.Result); } }; worker.RunWorkerAsync(); // 调用结果 int result = tcs.Task.Result; WriteLine($"结果是:{result}"); ReadLine(); } static int TaskMethod(string name, int seconds) { WriteLine($"任务{name}运行在线程{CurrentThread.ManagedThreadId}上. 是否为线程池线程{CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(seconds)); return 42 * seconds; }

运行结果如下图所示。

1533785637929

1.7 实现取消选项#

在TAP模式中,实现取消选项和之前的异步模式一样,都是使用CancellationToken来实现,但是不同的是Task构造函数允许传入一个CancellationToken,从而在任务实际启动之前取消它。

演示代码如下所示。

 

Copy

static void Main(string[] args) { var cts = new CancellationTokenSource(); // new Task时 可以传入一个 CancellationToken对象 可以在线程创建时 变取消任务 var longTask = new Task<int>(() => TaskMethod("Task 1", 10, cts.Token), cts.Token); WriteLine(longTask.Status); cts.Cancel(); WriteLine(longTask.Status); WriteLine("第一个任务在运行前被取消."); // 同样的 可以通过CancellationToken对象 取消正在运行的任务 cts = new CancellationTokenSource(); longTask = new Task<int>(() => TaskMethod("Task 2", 10, cts.Token), cts.Token); longTask.Start(); for (int i = 0; i < 5; i++) { Sleep(TimeSpan.FromSeconds(0.5)); WriteLine(longTask.Status); } cts.Cancel(); for (int i = 0; i < 5; i++) { Sleep(TimeSpan.FromSeconds(0.5)); WriteLine(longTask.Status); } WriteLine($"这个任务已完成,结果为{longTask.Result}"); ReadLine(); } static int TaskMethod(string name, int seconds, CancellationToken token) { WriteLine($"任务运行在{CurrentThread.ManagedThreadId}上. 是否为线程池线程:{CurrentThread.IsThreadPoolThread}"); for (int i = 0; i < seconds; i++) { Sleep(TimeSpan.FromSeconds(1)); if (token.IsCancellationRequested) { return -1; } } return 42 * seconds; }

运行结果如下图所示,这里需要注意的是,如果是在任务执行之前取消了任务,那么它的最终状态是Canceled。如果是在执行过程中取消任务,那么它的状态是RanCompletion

1533783996906

1.8 处理任务中的异常#

在任务中,处理异常和其它异步方式处理异常类似,如果能在所发生异常的线程中处理,那么不要在其它地方处理。但是对于一些不可预料的异常,那么可以通过几种方式来处理。

可以通过访问task.Result属性来处理异常,因为访问这个属性的Get方法会使当前线程等待直到该任务完成,并将异常传播给当前线程,这样就可以通过try catch语句块来捕获异常。另外使用task.GetAwaiter().GetResult()方法和第使用task.Result类似,同样可以捕获异常。如果是要捕获多个任务中的异常错误,那么可以通过ContinueWith()方法来处理。

具体如何实现,演示代码如下所示。

 

Copy

static void Main(string[] args) { Task<int> task; // 在主线程中调用 task.Result task中的异常信息会直接抛出到 主线程中 try { task = Task.Run(() => TaskMethod("Task 1", 2)); int result = task.Result; WriteLine($"结果为: {result}"); } catch (Exception ex) { WriteLine($"异常被捕捉:{ex.Message}"); } WriteLine("------------------------------------------------"); WriteLine(); // 同上 只是访问Result的方式不同 try { task = Task.Run(() => TaskMethod("Task 2", 2)); int result = task.GetAwaiter().GetResult(); WriteLine($"结果为:{result}"); } catch (Exception ex) { WriteLine($"异常被捕捉: {ex.Message}"); } WriteLine("----------------------------------------------"); WriteLine(); var t1 = new Task<int>(() => TaskMethod("Task 3", 3)); var t2 = new Task<int>(() => TaskMethod("Task 4", 4)); var complexTask = Task.WhenAll(t1, t2); // 通过ContinueWith TaskContinuationOptions.OnlyOnFaulted的方式 如果task出现异常 那么才会执行该方法 var exceptionHandler = complexTask.ContinueWith(t => { WriteLine($"异常被捕捉:{t.Exception.Message}"); foreach (var ex in t.Exception.InnerExceptions) { WriteLine($"-------------------------- {ex.Message}"); } },TaskContinuationOptions.OnlyOnFaulted); t1.Start(); t2.Start(); ReadLine(); } static int TaskMethod(string name, int seconds) { WriteLine($"任务运行在{CurrentThread.ManagedThreadId}上. 是否为线程池线程:{CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(seconds)); // 人为抛出一个异常 throw new Exception("Boom!"); return 42 * seconds; }

运行结果如下所示,需要注意的是,如果在ContinueWith()方法中捕获多个任务产生的异常,那么它的异常类型是AggregateException,具体的异常信息包含在InnerExceptions里面,要注意和InnerException区分。

1533785572866

1.9 并行运行任务#

本节中主要介绍了两个方法的使用,一个是等待组中全部任务都执行结束的Task.WhenAll()方法,另一个是只要组中一个方法执行结束都执行的Task.WhenAny()方法。

具体使用,如下演示代码所示。

 

Copy

static void Main(string[] args) { // 第一种方式 通过Task.WhenAll 等待所有任务运行完成 var firstTask = new Task<int>(() => TaskMethod("First Task", 3)); var secondTask = new Task<int>(() => TaskMethod("Second Task", 2)); // 当firstTask 和 secondTask 运行完成后 才执行 whenAllTask的ContinueWith var whenAllTask = Task.WhenAll(firstTask, secondTask); whenAllTask.ContinueWith(t => WriteLine($"第一个任务答案为{t.Result[0]},第二个任务答案为{t.Result[1]}"), TaskContinuationOptions.OnlyOnRanToCompletion); firstTask.Start(); secondTask.Start(); Sleep(TimeSpan.FromSeconds(4)); // 使用WhenAny方法 只要列表中有一个任务完成 那么该方法就会取出那个完成的任务 var tasks = new List<Task<int>>(); for (int i = 0; i < 4; i++) { int counter = 1; var task = new Task<int>(() => TaskMethod($"Task {counter}",counter)); tasks.Add(task); task.Start(); } while (tasks.Count > 0) { var completedTask = Task.WhenAny(tasks).Result; tasks.Remove(completedTask); WriteLine($"一个任务已经完成,结果为 {completedTask.Result}"); } ReadLine(); } static int TaskMethod(string name, int seconds) { WriteLine($"任务运行在{CurrentThread.ManagedThreadId}上. 是否为线程池线程:{CurrentThread.IsThreadPoolThread}"); Sleep(TimeSpan.FromSeconds(seconds)); return 42 * seconds; }

运行结果如下图所示。

1533793481274

1.10 使用TaskScheduler配置任务执行#

Task中,负责任务调度是TaskScheduler对象,FCL提供了两个派生自TaskScheduler的类型:线程池任务调度器(Thread Pool Task Scheduler)同步上下文任务调度器(Synchronization Scheduler)。默认情况下所有应用程序都使用线程池任务调度器,但是在UI组件中,不使用线程池中的线程,避免跨线程更新UI,需要使用同步上下文任务调度器。可以通过执行TaskSchedulerFromCurrentSynchronizationContext()静态方法来获得对同步上下文任务调度器的引用。

演示程序如下所示,为了延时同步上下文任务调度器,我们此次使用WPF来创建项目。

MainWindow.xaml 代码如下所示。

 

Copy

<Window x:Class="Recipe9.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Recipe9" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <TextBlock Name="ContentTextBlock" HorizontalAlignment="Left" Margin="44,134,0,0" VerticalAlignment="Top" Width="425" Height="40"/> <Button Content="Sync" HorizontalAlignment="Left" Margin="45,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonSync_Click"/> <Button Content="Async" HorizontalAlignment="Left" Margin="165,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonAsync_Click"/> <Button Content="Async OK" HorizontalAlignment="Left" Margin="285,190,0,0" VerticalAlignment="Top" Width="75" Click="ButtonAsyncOK_Click"/> </Grid> </Window>

MainWindow.xaml.cs 代码如下所示。

 

Copy

/// <summary> /// MainWindow.xaml 的交互逻辑 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } // 同步执行 计算密集任务 导致UI线程阻塞 private void ButtonSync_Click(object sender, RoutedEventArgs e) { ContentTextBlock.Text = string.Empty; try { string result = TaskMethod().Result; ContentTextBlock.Text = result; } catch (Exception ex) { ContentTextBlock.Text = ex.InnerException.Message; } } // 异步的方式来执行 计算密集任务 UI线程不会阻塞 但是 不能跨线程更新UI 所以会有异常 private void ButtonAsync_Click(object sender, RoutedEventArgs e) { ContentTextBlock.Text = string.Empty; Mouse.OverrideCursor = Cursors.Wait; Task<string> task = TaskMethod(); task.ContinueWith(t => { ContentTextBlock.Text = t.Exception.InnerException.Message; Mouse.OverrideCursor = null; }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.FromCurrentSynchronizationContext()); } // 通过 异步 和 FromCurrentSynchronizationContext方法 创建了线程同步的上下文 没有跨线程更新UI private void ButtonAsyncOK_Click(object sender, RoutedEventArgs e) { ContentTextBlock.Text = string.Empty; Mouse.OverrideCursor = Cursors.Wait; Task<string> task = TaskMethod(TaskScheduler.FromCurrentSynchronizationContext()); task.ContinueWith(t => Mouse.OverrideCursor = null, 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 = $"任务运行在{CurrentThread.ManagedThreadId}上. 是否为线程池线程:{CurrentThread.IsThreadPoolThread}"; Console.WriteLine(str); ContentTextBlock.Text = str; return str; }, scheduler); } }

运行结果如下所示,从左至右依次单击按钮,前两个按钮将会引发异常。
1533806840998

具体信息如下所示。

1533794812153

参考书籍

本文主要参考了以下几本书,在此对这些作者表示由衷的感谢,感谢你们为.Net的发扬光大所做的贡献!

  1. 《CLR via C#》
  2. 《C# in Depth Third Edition》
  3. 《Essential C# 6.0》
  4. 《Multithreading with C# Cookbook Second Edition》
  5. 《C#多线程编程实战》

源码下载点击链接 示例源码下载

笔者水平有限,如果错误欢迎各位批评指正!

本来想趁待业期间的时间读完《Multithreading with C# Cookbook Second Edition》这本书,并且分享做的相关笔记;但是由于笔者目前职业规划和身体原因,可能最近都没有时间来更新这个系列,没法做到几天一更。请大家多多谅解!但是笔者一定会将这个系列全部更新完成的!感谢大家的支持!

作者:InCerry

出处:https://www.cnblogs.com/InCerry/p/9450493.html

版权:本文采用「署名 4.0 国际」知识共享许可协议进行许可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/438387.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【VSCode快捷键大合集】

&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是超梦梦梦梦&#xff0c;很高兴认识大家~ &#x1f64f;如果本博文对小伙伴们有帮助的话&#xff0c;欢迎&#x1f50e;关注➕&#x1f91e;点赞➕&#x1f4cb;评论➕&#x1f604;收藏一波哦~ &…

浅谈Vue.js的优势

写在前面 今天小梦跟小伙伴们简简单单聊一下Vue.js的优势。小梦也是刚刚接触Vue.js&#xff0c;在学习一门新的技术之前&#xff0c;我们当然要了解其优势&#xff0c;知道优势在哪更加有利于我们去学习并转换为自己的储备。 浅谈Vue.js的优势 首先Vue.js是一个轻巧、高性能、…

async await 的前世今生(Updated)

async 和 await 出现在C# 5.0之后&#xff0c;给并行编程带来了不少的方便&#xff0c;特别是当在MVC中的Action也变成async之后&#xff0c;有点开始什么都是async的味道了。但是这也给我们编程埋下了一些隐患&#xff0c;有时候可能会产生一些我们自己都不知道怎么产生的Bug&…

MySQL八大约束

MySQL约束MySQL约束主键约束(primary key)自增长约束(auto_increment)非空约束(not null)唯一约束(unique)默认约束(default)零填充约束(zerofill)外键约束(foreign key)MySQL约束 概念 约束英文&#xff1a;constraint约束实际上就是表中数据的限制条件 作用 表在设计的时…

ASP.NET使用管道模型(PipleLines)处理HTTP请求

大多数人认为ASP.NET仅仅只是页面——使用模板来创建HTML页面然后返回给浏览器。但是这仅仅只是ASP.NET使用HTTP管道模型处理WEB程序很小的一方面。管道模型是类似于Web Services的一种在服务器端处理ASP.NET页面的框架技术。作为一名高级的ASP.NET的开发者&#xff0c;你必须清…

【LeetCode-SQL每日一练】——1.组合两个表

&#x1f388;写在前面 &#x1f64b;‍♂️大家好呀&#xff0c;我是超梦梦梦梦。小伙伴们都知道&#xff0c;不管是在学习中还是日常工作中&#xff0c;几乎天天是要跟数据库打交道的&#xff0c;为了更好的操作数据库&#xff0c;我们的SQL知识储备是必不可少的。想要掌握好…

【LeetCode-SQL每日一练】——2. 第二高的薪水

&#x1f388;写在前面 &#x1f64b;‍♂️大家好呀&#xff0c;我是超梦。小伙伴们都知道&#xff0c;不管是在学习中还是日常工作中&#xff0c;几乎天天是要跟数据库打交道的&#xff0c;为了更好的操作数据库&#xff0c;我们的SQL知识储备是必不可少的。想要掌握好SQL&am…

【LeetCode-SQL每日一练】—— 181. 超过经理收入的员工

&#x1f388;写在前面 &#x1f64b;‍♂️大家好呀&#xff0c;我是超梦。小伙伴们都知道&#xff0c;不管是在学习中还是日常工作中&#xff0c;几乎天天是要跟数据库打交道的&#xff0c;为了更好的操作数据库&#xff0c;我们的SQL知识储备是必不可少的。想要掌握好SQL&am…

【windows环境——VSCode安装教程】

大家好呀&#xff01;我是超梦&#xff0c;今天给小伙伴们带来一个最新版在windows环境的VSCode安装教程&#xff0c;话不多说我们开始吧。 VSCode安装下载与安装设置中文环境下载与安装 1. 第一步&#xff0c;进入官网&#xff0c;根据自己的电脑位数选择下载&#xff0c;小梦…

C#通用类Helper整理

★前言 最近下载了tita_chou在CSDN上传的一个资源&#xff0c;是在工作中整理的C#帮助类&#xff0c;里面包含了很多实用的类&#xff0c;想到我之前收集过自己用到少的可怜的类&#xff0c;心生敬意啊。当粗略的查看了那个资源&#xff0c;发现有一些是重复的&#xff0c;有一…

【LeetCode-SQL每日一练】—— 182. 查找重复的电子邮箱

&#x1f388;写在前面 &#x1f64b;‍♂️大家好呀&#xff0c;我是超梦。小伙伴们都知道&#xff0c;不管是在学习中还是日常工作中&#xff0c;几乎天天是要跟【数据库】打交道的&#xff0c;为了更好的操作数据库&#xff0c;我们的SQL【知识储备】是必不可少的。想要掌握…

【你会用代码画年兽吗】20行代码使用JS实现虎年春节倒计时 —— 红红火火过虎年

&#x1f34a;前言 &#x1f34a;哈喽小伙伴们好呀&#xff0c;我是超梦&#xff0c;大家可以叫我小梦~&#xff0c;马上就要过虎年春节啦&#xff0c;在新的一年里祝愿大家&#x1f42f;身体健康&#xff0c;&#x1f42f;平平安安&#xff0c;&#x1f42f;虎气冲天&#xf…

【LeetCode-SQL每日一题】——183. 从不订购的客户

&#x1f388;写在前面 &#x1f64b;‍♂️大家好呀&#xff0c;我是超梦。小伙伴们都知道&#xff0c;不管是在学习中还是日常工作中&#xff0c;几乎天天是要跟数据库打交道的&#xff0c;为了更好的操作数据库&#xff0c;我们的SQL知识储备是必不可少的。想要掌握好SQL&am…

c#进阶(1)—— Task Parallel Library 并行执行与串行执行

本文参考的博文出处&#xff1a;http://www.cnblogs.com/stoneniqiu/p/4857021.html 总体说明&#xff1a; &#xff08;1&#xff09;、理解硬件线程和软件线程 硬件线程也称为逻辑内核&#xff0c;一个物理内核可以使用超线程技术提供多个硬件线程。所以一个硬件线程并不代…

【leetcodeMySQL每周一练】- 5道题带你练习mysql

&#x1f388;写在前面 &#x1f64b;‍♂️大家好呀&#xff0c;我是超梦。小伙伴们都知道&#xff0c;不管是在学习中还是日常工作中&#xff0c;几乎天天是要跟数据库打交道的&#xff0c;为了更好的操作数据库&#xff0c;我们的SQL知识储备是必不可少的。想要掌握好SQL&am…

【读书笔记】.Net并行编程高级教程--Parallel

一直觉得自己对并发了解不够深入&#xff0c;特别是看了《代码整洁之道》觉得自己有必要好好学学并发编程&#xff0c;因为性能也是衡量代码整洁的一大标准。而且在《失控》这本书中也多次提到并发&#xff0c;不管是计算机还是生物都并发处理着各种事物。人真是奇怪&#xff0…

【除夕最炫烟花代码】—— HTML+JQuery实现2022跨年烟花特效

&#x1f387;前言 大家好呀&#xff01;我是小梦是✨。 除夕马上到来&#xff0c;激动的心颤动的手&#xff01;今年由于管控严厉&#xff0c;许多地方禁止燃放烟花爆竹&#xff0c;既然不让我们放&#xff0c;那我们就用代码来实现烟花特效&#xff01; 文末有彩蛋哦~ 烟花效…

【LeetCode-SQL每日一练】—— 595. 大的国家

&#x1f388;写在前面 &#x1f64b;‍♂️大家好呀&#xff0c;我是超梦。大家可以叫我小梦~ 小伙伴们都知道&#xff0c;不管是在学习中还是日常工作中&#xff0c;几乎天天是要跟数据库打交道的&#xff0c;为了更好的操作数据库&#xff0c;我们的SQL知识储备是必不可少的…

c#进阶(2)—— ASP.NET MVC 常用路由总结

1、URL模式 路由系统用一组路由来实现它的功能&#xff0c;这些路由共同组成了应用系统URL架构或方案&#xff0c;这种URL架构是应用程序能够识别并能对之做出响应的一组URL&#xff0c;当处理一个输入 请求时&#xff0c;路由系统的工作是将这个请求URL与一个模式进行匹配&am…

【爱心代码大全】——情人节表白代码送给她属于我们程序员的浪漫

&#x1f474;大多数人以为的程序员——发量少&#xff0c;身穿格子褂&#xff0c;坐在电脑前就是码字。一点也不懂浪漫&#xff01; 谁说我们不懂浪漫的&#xff1f; 不&#xff01; 你错了 程序员一旦浪漫起来&#xff0c;真没其他人啥事了&#xff01;&#xff01;&#xff…