多任务应用系统构建模式
计算密集型任务要进行大量的计算,消耗CPU资源,如视频解码等,启用与CPU核心数相同的并行任务数可最大化利用CPU资源和加快任务的执行;IO密集型任务,如网络、磁盘IO等,CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度),任务数适当增多,CPU效率将提高。
实现多任务有如下几种模式:
- 多进程模式:在多核CPU上运行多个进程(数量与CPU核心数相同)可充分利用多核CPU计算能力。由于系统同时运行的进程数少,因此系统调度也非常高效。但每个系统允许同时运行的最大进程数量是有限的;
- 多线程模式;
- 多进程 + 多线程模式;
- 多协程模式;
- 异步IO复用模式:也称事件驱动。Nginx就是支持异步IO复用的Web服务器,在单核CPU上采用单进程就可以高效地支持多任务。用异步IO复用来实现多任务也是一个主要的应用趋势,但是无法充分利用多核CPU的计算能力。
下文将通过对线程、LWT、协程等相关概念的介绍来反映这些构建模式的差异。
线程
线程通常由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成,是进程中的一个实体,是系统独立调度分派的基本单位;线程也有就绪、阻塞和运行三种基本状态。进程是系统资源分配的基本单位,创建、撤消、切换开销大,进程间通信IPC(Inter-Process Communication)复杂,在SMP、MPP、NUMA系统上同时运行多个线程并行执行更为合适。线程的实体包括程序、数据和线程控制块(Thread Control Block,TCB),TCB包括以下信息:
- 线程状态;
- 当线程不运行时,被保存的现场资源;
- 一组执行堆栈;
- 存放每个线程的局部变量主存;
- 访问同一个进程中的主存和其它资源。
SMP(Symmetric Multi-Processor)系统中有多个处理器(核心),每个处理器都有自己的控制单元、算术逻辑单元和寄存器,都可以通过某种互联机制(通常是系统总线)访问一个共享的主存和I/O设备,还可以通过存储器中共享地址空间中的消息和状态信息相互通信。SMP广泛应用于PC和移动设备领域,并行度很高,能够显著提升性能。在SMP系统中,只运行操作系统的一个拷贝,不同处理器被授权均匀访问(Uniform Memory Access,UMA亦称作统一寻址技术或统一内存存取)存储器的不同部分,但同一时间只能有一个处理器访问存储器。
单核CPU上运行多线程的锁问题:线程在被阻塞或睡眠状态下,其已经持有的资源访问权不能释放才能保证并发访问资源的最终一致性。因此,多个并发线程访问多个不同对象时,有可能导致各自同时获得一部分资源访问权,而等待对方释放另一部分资源访问权被阻死,于是死锁发生。
单核CPU上运行多线程的效率问题:在CPU密集型作业条件下,多线程的确不能提高性能,甚至更浪费时间;但是,在IO密集型作业条件下,多线程则可以提升性能。
线程有以下三种常见的模型:
-
内核级线程(Kernel-Level Thread):内核级线程驻留在内核空间,由操作系统调度器管理、调度和分派;内核级线程在其生命期内都将被映射到一个内核线程,一旦终止,两个线程都将离开系统;内核级线程不受用户态上下文的拖累,资源同步和数据共享比进程要低一些。如下图所示:
-
用户级线程(User-Level Thread):用户级线程的创建、调度、同步和销毁是通过库函数在用户空间完成的,对内核都是不可见的,因此无法被调度到处理器内核。在任意给定时刻进程都只能运行一个用户级线程,在整个程序执行过程中是进程而非其用户级线程与一个内核线程关联起来。用户级线程的创建、销毁、切换代价比内核级线程小得多,允许每个进程定制自己的调度算法,线程管理比较灵活,还能在不支持线程的操作系统中实现。如下图所示:
-
混合模型:在支持多线程的操作系统中,不同系统实现的线程模型并不相同,有的实现了用户级线程模型,有的实现了内核级线程模型,还有的二者兼具如下图:
轻量进程(Lightweight Process,LWP)
a LWP runs in user space on top of a single kernel thread and shares its address space and system resources with other LWPs within the same process.
LinuxThreads 所采用的是线程-进程1:1模型,即一个用户级线程对应一个轻量进程。LinuxThreads 的线程调度由内核完成,线程创建、同步、销毁由核外线程库(线程包)来完成。
NPTL(Native Posix Thread Library) 作为 LinuxThreads 之后的替代者仍然采用1:1的线程模型,NPTL没有使用管理线程,LWP的管理直接放在核内进行。NPTL仍然不是100% POSIX兼容的,但就性能而言相对 LinuxThreads 已经有很大程度上的改进了。
协程(Coroutine)
协程又称微线程,一个线程可以包含一个或多个协程,协程的相关特性如下:
- 运行在用户态,是系统内核不可感知的,有自己的目态栈,相当于用户级线程;
- 异步编码结构同步化;
- 从内部挂起主动释放CPU而非线程的抢占式调度切换,共享资源不加锁;
- 多个协程在同一个线程上下文中执行,同一时间只有一个是处于**态,其他都是暂停态(suspended),无法充分利用多核CPU的计算能力;
- 目态栈数据量小,切换更快。
其实,协程先于抢占式调度切换的线程出现用来模拟多任务并发,但由于其主动释放CPU而非抢占式(Non-Preempt)调度切换导致多任务时间片不均衡,而后线程出现了;协程近年来又开始出现流行的趋势。win32中称协程为纤程(Fiber),主要用于旧的Unix应用的移植,创建新应用还是建议优先使用线程。
最后的总结语
- 在支持多线程的系统中,进程是系统资源分配的基本单位,线程是系统独立调度的基本单位;
- 无论系统支持什么样的线程模型,操作系统调度器指派到处理器内核进行处理的对象是内核线程;
- 用户级线程在用户空间调度灵活高效,内核级线程在内核空间调度成本更高;
- 用户级线程在本进程内竞争CPU处理资源,内核级线程在全系统内竞争CPU处理资源,因此后者才能发挥SMP、MPP、NUMA并行处理的优势;
- 进程的创建、撤消、切换成本都高于线程,故使用多个线程比使用多个进程更有管理维持上的性能优势;
- 由于多线程共享进程资源会导致任何一个线程挂掉都可能造成整个进程崩溃,多进程应用程序在出现进程池内的进程崩溃或被攻击的情况下表现更加健壮,一个子进程崩溃不会影响主进程和其他子进程。因此Chrome浏览器为每一个标签页运行一个进程,Apache最早也是采用多进程模式。在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式;
- 协程切换快,不存在锁安全问题,异步编码结构同步化,要充分利用多核CPU的计算能力,还要以多进程或多线程为载体。
- 异步I/O复用不切换线程或进程,效率很高。