C# 多线程与异步编程

写在前面

自从介入UI界面编程以后,就免不了使用多线程处理问题,而一直一直以来却并没有仔细了解多线程和异步究竟有什么区别,这篇文章就以现在的认知来聊一聊。

异步编程是怎么运行的?

阅读

来看一篇文章

Asynchronous programming is a bit more general in that it has to do with latency (something on which your application has to wait, for one reason or another), whereas multithreaded programming is a way to achieve parallelization (one or more things that your application has to do at the same time). That said, the two topics are closely related; an application that performs work on multiple threads in parallel will often need to wait until such work is completed in order to take some action (e.g. update the user interface). So, this idea of waiting is the more general characteristic that is referenced by the term asynchronous, regardless of thread count.

异步更强调等待完成,多线程更强调并行
再来看MSDN上的这篇文章,提到了在C#从语言层面支持了异步编程:async/await:

对于 I/O 绑定代码,等待一个在 async 方法中返回 Task 或 Task 的操作。
对于 CPU 绑定代码,等待一个使用 Task.Run 方法在后台线程启动的操作。

也就是说,异步本身并不会启用新线程,“应用await关键字后,它将挂起调用方法,并将控制权返还给调用方,直到等待的任务完成。”
以及这篇文章,详细讲解了async/await的控制流:
C# 多线程与异步编程
大概流程就是:

  1. 外部代码调用async Task方法GetUrlContentLengthAsync
  2. GetUrlContentLengthAsync内部又调用了另一个异步方法GetStringAsync
  3. 由于GetStringAsync有下载等一些操作,为了避免阻塞,就在执行的同时将程序的控制权返还给外部代码,这时候外部程序就可以继续执行
  4. 执行到DoIndependentWork的前一句并没有await,所以并不会等待GetStringAsync的结果如何而是会接着往下执行
  5. 执行DoIndependentWork
  6. 最外层方法GetUrlContentLengthAsync已经执行完了它能执行的所有操作,下一步就是将计算结果返回,但是这时候并没有结果供它返回,因此加await等待
  7. getStringTask拿到结果
  8. 将结果存储到task中

理解

关于异步方法中的线程

异步方法不会创建额外的线程。它运行在当前同步的上下文中,仅使用该方法在当前线程上**时的时间。

关于async and await

最好成对出现,如果仅有async没有await,程序将按照同步执行。

异步和多线程的区别

这里就要贴MSDN的另一篇文章了。文章中提到,假如要用程序来模拟“做早餐”这一过程,那么多线程就好比是多名厨师同时完成不同的工作并协调公共资源,而异步则好比是single one利用煎蛋的时间烤培根:

Cooking breakfast is a good example of asynchronous work that isn’t parallel. One person (or thread) can handle all these tasks. Continuing the breakfast analogy, one person can make breakfast asynchronously by starting the next task before the first completes. The cooking progresses whether or not someone is watching it. As soon as you start warming the pan for the eggs, you can begin frying the bacon. Once the bacon starts, you can put the bread into the toaster.
For a parallel algorithm, you’d need multiple cooks (or threads). One would make the eggs, one the bacon, and so on. Each one would be focused on just that one task. Each cook (or thread) would be blocked synchronously waiting for bacon to be ready to flip, or the toast to pop.

再来看*上这位老哥的描述
You are cooking in a restaurant. An order comes in for eggs and toast.

  • Synchronous: you cook the eggs, then you cook the toast.
  • Asynchronous, single threaded: you start the eggs cooking and set a timer. You start the toast cooking, and set a timer. While they are both cooking, you clean the kitchen. When the timers go off you take the eggs off the heat and the toast out of the toaster and serve them.
  • Asynchronous, multithreaded: you hire two more cooks, one to cook eggs and one to cook toast. Now you have the problem of coordinating the cooks so that they do not conflict with each other in the kitchen when sharing resources. And you have to pay them.

再贴另一篇神作说明There Is No Thread。大意就是,在CPU内核中处理这些代码,这时候已经没有线程的概念可言了。

写在后面

当需要CPU-bound操作时,Task.Run()可以开一个线程池线程,这就很接近Thread了。
C# 多线程与异步编程
不同的是,这里仍然是非阻塞式等待,在await语句之后的句子仍可执行,上述例子就是多线程异步,如果这里不使用异步而是只开一个单独的线程,那么就需要访问公共变量来告诉caller当前执行完毕与否,这样的话就又涉及到锁的安全问题了。

总结

  • 多线程和异步有非常多的相似之处,但是本质并不相同;
  • 尽可能多地使用非阻塞式等待替代阻塞式等待。