什么是线程、进程?

线程是对物理CPU进行虚拟化,也是操作系统能调度的最小单位。

进程实际是应用程序的实例要使用的资源的集合,每个进程都被赋予了一个虚拟地址空间来避免被其它进程访问。

一个线程确定由某一进程拥有,一般不能跨进程。

线程结构

线程有空间(内存耗用)和时间(上下文调度)上的开销。

内存耗用

① 线程内核对象 (thread kernel object)

主要是有对线程的描述属性。x64使用约1240字节。

值得注意的是线程上下文(thread context):线程上下文是是线程上一次执行完毕后,CPU寄存器的状态(内存块)。

② 线程环境块 (thread environment block,TEB)

有GDI(图形设备接口)和OpenGL用的一些数据,以及异常处理链首:线程每进入一个try块,都会在链首(head)中插入一个节点(node),退出try块时删除该节点。x64中4KB。

③ 用户模式栈 (user- mode stack)

堆栈概念中的栈说的就是这个了,默认分配1MB内存(其实windows是保留1MB容量,等用了才调拨给你)。

④ 内核模式栈 (kernel- mode stack)

应用程序代码向OS中的内核模式函数传递实参时,会复制 用户模式栈 传去的实参并加以验证并不允许修改。最后OS内核代码开始处理复制的值。分配x86是12KB,x64是24KB。

上下文调度

Windows任何时刻都只将一个线程分配给一个CPU(或CPU核,下面称CPU)。

CPU会为线程执行一个**时间片 (quantum)**的时长,大概30ms吧,等时间片到期了,就会进行上下文调度切换执行另一个线程。

上下文调度具体流程:

  1. 将CPU寄存器的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中。
  2. 从现有线程集合中选出一个线程供调度。如果该线程由另一个进程拥有(而不是CPU上一次调度的线程的所属进程), Windows在
    开始执行任何代码或者接触任何数据之前,还必须切换CPU“看见”的虚拟地址空间。
  3. 将所选上下文结构中的值加载到CPU的寄存器中。

上下文调度之后线程切换完成,CPU就会执行所选的线程,直到下一个时间片过了又要切换。

这是净开销,只是为了能够提供一个健壮的、响应灵敏的操作系统。具体就是cpu运行的某个线程卡死了,过一个时间片后也会被分配其他线程执行(比如任务管理器线程来终止卡死程序)。

线程优先级

系统调度CPU执行哪一个线程,是由线程自己的优先级决定的,所以可能出现高优先级线程太多导致长时间没有处理低优先级线程,这种情况叫饥饿

低优先级的线程哪怕时间片没用完,也会被立刻挂起执行优先级更高的线程,这是Windows称作抢占式多线程操作系统的原因。

优先级是0~31。

0是系统启动时会创建一个特殊的零页线程,优先级为0,在没有其他线程需要“干活儿”的时候,零页线程将系统RAM的所有空闲页清零。

自己创建的线程可以通过ThreadA.Priority = ThreadPriority.AboveNormal;的方式,指定优先级。

线程池

线程的执行是事件分发式的,也就是内部维护一个操作请求队列。

生命流程

CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列。应用程序执行一个异步操作时,就会对线程池进行请求,具体是将一个记录项(entry)追加到队列中。

线程池会从这个队列中提取记录项,将这个记录项派发(dispatch)给一个线程池的线程;如果线程池没有线程,就创建一个新线程。当这个线程完成任务后并不销毁,而是返回线程池、进入空闲状态。

但是如果一个线程在线程池里闲太久了(应用程序很久不向线程池发出请求),为了避免资源浪费,CLR才会终止它。

调度机制

也就是具体是怎么分发任务的。

(线程池)全局队列:当调用ThreadPool.QueueUserWorkItem、Task时,新追加的任务会被添加进全局队列中,全局队列采用先进先出FIFO的方式,让工作者线程们自己去取任务。全局队列使用线程同步锁,这是为了避免多个线程同时取到一个任务。

(线程)本地队列:在全局队列中领完的任务会放入工作者线程各自的本地队列,本地队列采用后入先出LIFO的方式来执行任务。本地队列一般不锁,因为只有对应的工作者线程访问它。

如果工作者线程发现自己本地队列空了,就会尝试从另一个工作者线程的本地队列“偷”一个Task。这个Task在本地队列的队尾,并会要求获取一个线程同步锁。

如果所有本地队列都空了,工作者线程会使用FIFO算法从全局队列取出一个工作项并获得它的锁。

如果全局队列也为空,工作者线程会进入睡眠状态。

如果工作者线程睡眠时间很长,它会自己醒来并销毁自身,释放线程使用的资源(内核、栈等)。

取消任务

可以使用辅助类CancellationToken,它有一个bool字段可以用来判断是否取消执行了任务,只要执行Cancel方法就可以让他变成false。

自己传到线程里判断就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// 注册回调
token.Register(() => Console.WriteLine("Count is cancelled, thread " + Thread.CurrentThread.ManagedThreadId));
ThreadPool.QueueUserWorkItem(_ => Count(token, 1000));

Console.WriteLine("Press <Enter> to cancel Thread Works");
Console.ReadLine();
cts.Cancel();

Console.Read();
}


private static void Count(CancellationToken token, int countTo)
{
for (int count = 0; count < countTo; count++)
{
// 当Source执行Cancel时,会变成false
if (token.IsCancellationRequested)
{
Console.WriteLine("break, thread " + Thread.CurrentThread.ManagedThreadId);
break;
}

Console.WriteLine(count);
Thread.Sleep(200);
}
}