多线程相关概念汇总

来源:互联网 发布:淘宝放心淘加入条件 编辑:程序博客网 时间:2024/06/07 10:39

转载至:https://blog.gkarch.com/threading/concepts.html#async

1基础Permalink

1.1线程Permalink

线程(thread) 是操作系统能够调度的最小执行单元。不过我更喜欢把它理解为,这是操作系统对于 CPU 计算资源的抽象。

如同虚拟内存,程序看到的空间资源是操作系统抽象出来的虚拟空间。而看到的时间资源就是操作系统抽象出来的线程了。在早期 CPU 还是单核的时候,如果没有这种抽象,就不可能实现多任务。这样的抽象也能让程序的开发者不用考虑 CPU 具体的情况,而是根据程序需要自由的使用计算资源。

这样,有了虚拟内存和线程,程序在时间和空间上都有了一个硬件无关的环境。

抢占Permalink

因为有了线程这样的抽象,导致需要同时执行的代码要远大于 CPU 核心数,这种供不应求就需要一种机制保证多个线程能够共享一个核心。

操作系统并不清楚线程要做什么,也不清楚线程要执行多久,所以一种比较合理的方式就是一会儿执行这个,一会儿执行那个。就相当于把 CPU 核心的时间切分成开,然后把它们分配给线程使用,让线程与核心轮流发生关系。

线程并不清楚这次时间还剩多久,也不清楚下次轮到自己是什么时候,只要操作系统判断时间到了,线程就会被暂停执行,操作系统选择下一个线程来执行。在线程看来,就是自己的时间被 抢占(preempt) 了。

被操作系统切分开的时间叫做 时间切片(时间片,time-slice,quantum),切片的长度由操作系统控制,是系统定时器精度的整数倍(Windows 系统默认 15.625ms,可以修改)。

上下文切换Permalink

在抢占式多任务系统中,由于线程并不清楚自己在什么时候会被打断,所以必须由操作系统保存线程当时的状态,之后再次运行它的时候恢复之前的状态,这样线程才能正常继续执行。

上下文切换(context-switch) 就是指操作系统暂停线程执行,保存其状态,恢复下一个执行的线程的状态,开始其执行的过程。这个过程大概需要几微秒,算是开销比较大的操作。这里的状态主要是指 CPU 寄存器的值(包括指令指针和栈指针),还有一些操作系统需要的数据。

上面主要说的是时间片结束时的情况,还有种情况就是线程主动等待,这时仍然需要上下文切换,在后面阻塞和自旋中会再次提到。另外,系统中断也会导致一种轻量级的上下文切换,不过中断处理不能算线程,在后面的系统中断中会进行描述。

协作Permalink

多个线程共享一个核心还有另一种方式,就是 协作(cooperation)。意思是操作系统不能打断线程的执行,而是线程在其觉得合适的时机主动 出让(yield),这时操作系统才能选择下一个执行的线程。

很明显的问题是,如果正在执行的线程因为各种原因不出让,那么其它线程就无法执行。这在操作系统层面一般是无法接受的,所以绝大多数操作系统都是抢占式的。不过这种协作的机制可以无需上下文切换,或者大幅降低上下文切换的开销,系统调度起来非常容易。因此如果能够用好,性能会很高。所以在操作系统整体是抢占式的前提下,往往还有一定协作机制,可能由操作系统实现,也可能由编程语言或平台实现。往往被称为 协程(coroutine),之后会进一步说明。

计算密集 vs I/O 密集Permalink

在讨论多线程的场景下,工作分为两种类型:

计算密集(compute-intensive,或 CPU-bound)
顾名思义,代码的执行需要大量占用 CPU 时间,一般来说就是进行循环计算的操作。比如查找、排序、统计、编码解码等等这些与外设无关,只使用 CPU 和内存的算法。换句话说就是代码的执行主要受 CPU 计算能力限制(极端优化性能时,CPU 缓存也非常重要)。
I/O 密集(I/O-intensive,或 I/O-bound)
代码大量的时间用于进行 I/O。比如涉及数据库、网络、磁盘、串口、USB或者其它设备的输入输出,需要发起请求,等待结果,或者响应请求,传递数据。也就是代码的执行主要受 I/O 能力限制。

由于它们的依赖不同,所以不能使用同样的多线程方式来处理,而应该区别对待。比如对于计算密集的工作,最多使用 CPU 核心数量的线程来计算就足够了,它们会让 CPU 满负荷工作,线程再多只能增加系统负担,降低效率。而对于 I/O 密集的工作,应该尽可能使用操作系统提供的异步机制,而不是让大量线程浪费在 I/O 等待上。

1.2线程池Permalink

线程在时间和空间上都属于昂贵的资源。时间上之前有提到,进行上下文切换需要几微秒,而且如果频繁进行阻塞,浪费剩余时间片,开销会更大;空间上,Windows 上一个线程默认占用 1MB 内存空间用于线程独立的栈,意味着如果是 32 位程序,最多同时使用 2000 多个线程。

因此,需要一种机制能够让线程相对合理的被使用,既能充分利用硬件资源,又能避免进行过多的线程创建与销毁。线程池(thread pool) 就是这种机制的实现,它可能由操作系统提供,也可能由语言或平台提供,也可能由第三方或自己实现。

线程池管理工作线程,使用者把任务(需要执行的代码)交给线程池,也就是加入线程池的任务队列,工作线程完成之前的任务后,就继续从队列中取任务执行。如果没有工作线程空闲,而队列中还有任务,线程池就可能会创建新的工作线程来处理任务。而如果工作线程空闲太久,就会被销毁。

实际的实现可能更复杂,比如每个工作线程都有自己的任务队列,降低了在队列上同步的需要;创建新的工作线程时可能会等待,避免为大量短时间任务创建大量线程。但因为线程池不清楚任务要执行多久,不清楚任务总体的规模,也不清楚任务是计算密集还是 I/O 密集的,所以并不一定能够提供非常好的效果。一般的原则是:短时间的和少量并发的任务可以交给线程池,而长时间或大量并发的任务最好自己处理,来达到更好的效果。

2并行 vs 并发Permalink

并行(parallelism) 与 并发(concurrency) 是非常常见的术语,然而使用时经常会混淆,这里简单说明下。

它们的共同点是,都描述了同时执行代码的行为。然而这里的“同时”具有不同的含义。

对于并行来说,同时是指真正的同时。也就是说,单核 CPU 是无法并行的。它更强调的是对计算资源的利用,效果是让程序执行的更快。比如:修改一个算法让它支持并行执行;让多台机器并行处理请求;使用 GPU 并行计算。

对于并发来说,同时是指多个任务在处理过程中有重叠,让外界看起来是同时的。无论单核还是多核 CPU 都可以并发。它更强调的是程序的可用性,与是否加速无关。比如:操作系统的时间切片就是一种支持多任务并发的机制;为保证 UI 响应,使用后台线程并发加载数据;服务器并发处理请求。

一个更宽泛的概念就是上面已经用到的 同时(simultaneous),它不算术语,所以也没有特殊的含义,可以用来描述不需要区分并行和并发的场景。比如:同时执行任务 A 和任务 B;同时发起 10 个请求。

3线程同步Permalink

3.1同步上下文Permalink

4PFXPermalink

5异步Permalink

5.1IOCPPermalink

6高级Permalink

6.1协程Permalink

6.2系统中断Permalink

from GKarch 博客


原创粉丝点击