承接上文。
线程池
线程池主要有两个好处:
- 避免线程创建和销毁的开销。
- 自动缩放:可以按需增减线程的数量。
总之,Windows系统自带了线程池的功能,通常情况下,你不可能有更好的实现。所以只需了解如何使用。
Windows的线程池有两种,分别是非托管线程池和托管线程池(即.NET线程池)。下面分别来介绍。
非托管线程池
- 每个进程都有一个线程池,线程池有个IOCP。
-
其中的线程分为IO线程和工作者线程(或非IO线程)。
- 其中工作者线程监听线程池的IOCP。
- IO线程专门执行APC的异步完成例程。在空闲时一直是可唤醒状态。
- 调用Windows API
QueueUserWorkItem
会让一个监听在IOCP上的工作者线程醒来,并执行例程。 - 调用
BindIOCompletionCallback
把一个文件句柄绑定到线程池的IOCP上。当此文件有关的IO操作完成时,一个工作者线程会被唤醒来执行后面的操作。 - 调用
QueueUserWorkItem
并传入WT_EXECUTEINPERSISTENTTHREAD
标识时,会将一个APC回调放入IO线程的APC队列中。
托管线程池
- 每个CLR都有一个线程池,线程池有个IOCP。(一般来说一个进程有一个CLR,也可能有多个)
-
其中的线程分为工作者线程和IO线程。
- 工作者线程从任务队列中取得任务并执行(不通过IOCP)。
- IO线程则监听IOCP,在唤醒时执行任务的ContinueWith集合中的任务。
- 可以调用
Task.Run()
或ThreadPool.QueueUserWorkItem()
来添加任务到任务队列中。 - 使用
ThreadPool.UnsafeQueueNativeOverlapped()
可以将任务添加到IO线程中。但很少使用。
可见,托管与非托管线程池的差距是巨大的。
.NET多线程及异步编程模型
- .NET 3.5及之前
只有Thread
,ThreadPool
等接口。直接操作线程。 - .NET 4
引入了Task
。 - .NET 4.5.1 (C# 5.0)
引入了TAP(基于Task的异步模式),引入了async/await
。
下面来着重分析Task编程模式。
Task初级
Task
最重要的两个方法是Task.Run()
和new Task().Start()
。使用这两个方法:
- 调用者传递一个委托给Task。
- 线程池的一个线程会执行此委托。
- 返回值将保存在
Task.Result
中。 - 调用
Task.Wait()
或Task.Result
会阻塞地等待工作线程执行结束。
以下图示演示了这个过程具体经历了什么。
在任务完成时继续执行其他Task
调用Task.ContinueWith(continueAction, continueOptions)
。其中,ContinuationOptions是一个枚举,提供了更多选项。
这个过程如何理解呢?请看下图。
Task里面有什么
以下是一些主要的属性:
- Id
- State
- Reference of parent task
- Reference of Task scheduler
- Delegate
- AsyncState (to pass method’s objects)
- ExecutionContext
- CancellationToken
- Collection of ContinueWithTask
async/await的机制
它实际上是Task.ContinueWith()
的语法糖。
为此我们来看一段程序:
private async void btnDoStuff_Click()
{
lblStatus.Content = "Doing Stuff";
await Task.Delay(4000);
lblStatus.Content = "After await";
}
它相当于
private void btnDoStuff_Click()
{
lblStatus.Content = "Doing Stuff";
Task t = Task.Delay(4000);
t.ContinueWith(task => lblStatus.Content = "After await");
}
但是ContinueWith
里面的是一个委托,最好写成函数。另写一个函数最简单,不过C#使用另一种“状态机”技术把这个函数写在了原来的方法内。也就是说,第一段代码实际上会被编译为(并不精确):
private void btnDoStuff_Click(int step)
{
switch (step)
{
case 0:
lblStatus.Content = "Doing Stuff";
Task t = Task.Delay(4000);
t.ContinueWith(task => btnDoStuff_Click(task.Step));
break;
case 1:
lblStatus.Content = "After await";
break;
}
}
方法被添加了一个参数step
, 第一次调用方法时step
为0,此后每进入一次则加一。由此则用一个方法实现了两端逻辑,这便是状态机重入技术。C#的另一个语法糖:用yield
实现IEnumerable
接口也是采用这种技术。
Execution Context 执行上下文
它在多线程的好比空气:你可以不知道它,但它非常重要。ExecutionContext
是为了解决线程本地存储在多线程中无法传递的问题:总得有一种机制能够传递全局信息。否则只能通过函数调用参数传递了。
当一个线程发起异步调用的时候,ExecutionContext
会自动的在线程之间传递以下信息:
- 线程安全设置
- Host设置(与web服务有关)
- Logical Call Context, 可以在其中保存和传递对象。
- 线程的Culture(从.NET 4.6以后)。
Synchronization Context 同步上下文
它是为了描述异步调用返回时的行为所创建的抽象。它有两个基本接口方法:
-
Send
同步地等待任务执行完毕。 -
Post
把任务发出去就不管了。
那么异步调用返回时的行为是什么意思?既然是抽象,那就会有具体的实现。后面我们会看到几种实现。
当开始异步调用时,C#会捕获(capture)当前线程的同步上下文,并保存到Task中。在异步调用返回时,需要恢复(resume)同步上下文。此时就会调用同步上下文的Send
或者Post
。
下面是几种典型的同步上下文实现:
- UI同步上下文。由于UI界面操作必须在UI线程中进行,因此这个上下文做的事情就是把需要恢复的工作Marshal起来交给UI线程。(可能有人会好奇如何交给UI线程去做。简单来说, UI线程有个Windows消息循环,同步上下文将任务封装在一个特定消息中,UI线程得到这个消息后,就去执行其中的任务)。
-
ASP.NET同步上下文。它有以下特点:
- 不会切换线程,因为后台线程没什么区别。
- 会把线程的Principle和Culture传递过去。(因为ASP.NET依赖于此)
- 在异步页面中记录尚未完成的IO数量。
- 默认同步上下文。就是线程池的调度器,基本上没有特别的操作。
最后,调用ConfigureAwait(false)
时,就会跳过恢复同步上下文这一过程。所以,有时候必要(当没必要传递任何信息时,使用它可以提高效率),有时候又会出错。例如,UI程序的异步调用本来没问题,你加了这个语句,反而会造成修改界面的操作可能不在UI线程中执行,从而出错。但是注意,无论如何,执行上下文都是会传递的。
结合以上,第一段程序的更精确的编译后版本是这样的:
private void btnDoStuff_Click(int step)
{
switch (step)
{
case 0:
lblStatus.Content = "Doing Stuff";
Task t = Task.Delay(4000);
t.ContinueWith(
task => SynchronizationContext.Current.Post(
state => btnDoStuff_Click(task.Step),
task)
);
break;
case 1:
lblStatus.Content = "After await";
break;
}
}
到底是谁在执行异步调用?
这个问题曾经困扰我很久。如果我当前的线程调用一个异步调用后返回了,那到底是谁在完成真正调用的工作呢?答案是一个(或几个)共享的线程:线程池中的IO线程。
如下是一段代码:
async void GetButton_OnClick(object o, EventArgs e)
{
Task<Image> task = GetFaviconAsync(_url);
Image image = await task;
AddAFavicon(image);
}
async Task<Image> GetFaviconAsync(string url)
{
var task = _webClient.DownloadDataTaskAsync(url);
byte[] bytes = await task;
return MakeImage(bytes);
}
线程的执行情况如下图:
大部分的时间都在用户线程中。只有调用到非常底层,IO完成之后,才有IO线程被唤醒(见11),然后它调用Task的同步上下文的Post
,将剩下的任务再交给用户线程去执行。
下面是一个动态的解释:
ASP.NET应用的异步线程模型
关于IIS的架构和工作过程,有一些资料,这里也不打算深究。提供两张图,以供理解。
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。