并发编程 - 异步 vs. 多线程代码
并行编程是一个广泛的术语,我们应该通过观察异步方法和实际的多线程之间的差异展开探讨。 尽管 .NET Core 使用了任务来表达同样的概念,一个关键的差异是内部处理的不同。 调用线程在做其他事情时,异步方法在后台运行。这意味着这些方法是 I/O 密集型的,即他们大部分时间用于输入和输出操作,例如文件或网络访问。 只要有可能,使用异步 I/O 方法代替同步操作很有意义。相同的时间,调用线程可以在处理桌面应用程序中的用户交互或处理服务器应用程序中的同时处理其他请求,而不仅仅是等待操作完成。
计算密集型的方法要求 CPU 周期工作,并且只能运行在他们专用的后台线程中。CPU 的核心数限制了并行运行时的可用线程数量。操作系统负责在剩余的线程之间切换,使他们有机会执行代码。 这些方法仍然被并发地执行,却不必被并行地执行。尽管这意味着方法不是同时执行,却可以在其他方法暂停的时候执行。
并行 vs 并发
本文将在最后一段中重点介绍 在 .NET Core中多线程并发编程。
任务并行库
.NET Framework 4 引入了任务并行库 (TPL) 作为编写并发代码的首选 API。.NET Core采用相同的编程模式。 要在后台运行一段代码,需要将其包装成一个 任务:
var backgroundTask = Task.Run(() => DoComplexCalculation(42)); // do other work var result = backgroundTask.Result;
当需要返回结果时,Task.Run 方法接收一个 函数 (Func) ;当不需要返回结果时,方法 Task.Run 接收一个 动作 (Action) 。当然,所有的情况下都可以使用 lambda 表达式,就像我上面例子中调用带一个参数的长时间方法。 线程池中的某个线程将会处理任务。.NET Core 的运行时包含一个默认调度程序,使用线程池来处理队列并执行任务。您可以通过派生 TaskScheduler 类实现自己的调度算法,代替默认的,但这超过本文的讨论范围。 正如我们之前所见,我使用 Result 属性来合并被调用的后台线程。对于不需要返回结果的线程,我可以调用 Wait() 来代替。这两种方式都将被堵塞到后台任务完成。 为了避免堵塞调用线程 ( 如在ASP.NET Core应用程序中) ,可以使用 await 关键字:
var backgroundTask = Task.Run(() => DoComplexCalculation(42)); // do other work var result = await backgroundTask;
这样被调用的线程将被释放以便处理其他传入请求。一旦任务完成,一个可用的工作线程将会继续处理请求。当然,控制器动作方法必须是异步的:
public async Task<iactionresult> Index() { // method body }
处理异常
将两个线程合并在一起的时候,任务抛出的任何异常将被传递到调用线程中:
如果使用 Result 或 Wait() ,它们将被打包到 AggregateException 中。实际的异常将被抛出并存储在其 InnerException 属性中。
如果您使用 await,原来的异常将不会被打包。
在这两种情况下,调用堆栈的信息将保持不变。
取消任务
由于任务是可以长时间运行的,所以你可能想要有一个可以提前取消任务的选项。实现这个选项,需要在任务创建的时候传入取消的令牌 (token),之后再使用令牌触发取消任务:
var tokenSource = new CancellationTokenSource(); var cancellableTask = Task.Run(() => { for (int i = 0; i < 100; i++) { if (tokenSource.Token.IsCancellationRequested) { // clean up before exiting tokenSource.Token.ThrowIfCancellationRequested(); } // do long-running processing } return 42; }, tokenSource.Token); // cancel the task tokenSource.Cancel(); try { await cancellableTask; } catch (OperationCanceledException e) { // handle the exception }
实际上,为了提前取消任务,你需要检查任务中的取消令牌,并在需要取消的时候作出反应:在执行必要的清理操作后,调用 ThrowIfCancellationRequested() 退出任务。这个方法将会抛出 OperationCanceledException,以便在调用线程中执行相应的处理。
协调多任务
如果你需要运行多个后台任务,这里有些方法可以帮助到你。 要同时运行多个任务,只需连续启动它们并收集它们的引用,例如在数组中:
var backgroundTasks = new [] { Task.Run(() => DoComplexCalculation(1)), Task.Run(() => DoComplexCalculation(2)), Task.Run(() => DoComplexCalculation(3)) };
现在你可以使用 Task 类的静态方法,等待他们被异步或者同步执行完毕。
// wait synchronously Task.WaitAny(backgroundTasks); Task.WaitAll(backgroundTasks); // wait asynchronously await Task.WhenAny(backgroundTasks); await Task.WhenAll(backgroundTasks);