一、进程、线程、协程
1.1、进程
进程: 是系统进行资源分配的一个独立单位,内核通过进程控制块(PCB,process control block)来感知进程。
一个计算机系统进程包括(或者说“拥有”)下列资料:
- 那个程序的可执行机器代码的一个在存储器的映像。
- 分配到的存储器(通常是虚拟的一个存储器区域)。存储器的内容包括可执行代码、特定于进程的资料(输入、输出)、调用堆栈、堆栈(用于保存运行时运输中途产生的资料)。
- 分配给该进程的资源的操作系统描述符,诸如文件描述符(Unix术语)或文件句柄(Windows)、资料源和资料终端。
- 安全特性,诸如进程拥有者和进程的权限集(可以容许的操作)。
- 处理器状态(内文),诸如寄存器内容、物理存储器寻址等。当进程正在执行时,状态通常存储在寄存器,其他情况在存储器。
1.2、线程
-
是独立调度和分派的基本单位。内核通过线程控制块(TCB,thread control block)来感知线程。
-
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
1.3、线程的实现和调度方式
1.3.1、线程的实现
-
内核支持线程(KST,Kernel Supported Threads)
- 内核级线程的 TCB 保存在内核空间,其创建、阻塞、撤销、切换等活动也都是在内核空间实现的。
- 内核线程的调度是由内核完成的,一般是抢占式调度。
-
用户级线程(UST, User Supported Threads)
- 用户级线程则是内核无关的,用户级线程的实现在用户空间,内核感知不到用户线程的存在。
- 用户线程的调度算法可以是进程专用的,不会被内核调度,但同时,用户线程也无法利用多处理机的并行执行。调度也发生在用户态,一般是由线程库或编程语言运行时自行实现的。
- 一个拥有多个用户线程的进程,一旦有一个线程阻塞,该进程所有的线程都会被阻塞。
- 内核的切换需要转换到内核空间,而用户线程不需要,所以前者开销会更大。
- 用户线程需要内核的支持,一般是通过运行时系统或内核控制线程来连接一个内核线程,有 1:1、1:n、n:m 的不同实现。
1.3.2、调度方式
在分时操作系统中,处理机的调度一般基于时间片的轮转(RR, round robin),多个就绪线程排成队列,轮流执行时间片。所以进程调度主要有抢占式调度和协作式调度两种:抢占式(Preemptive) 与 协作式(Cooperative)。
-
抢占式(Preemptive) 抢占式调度往往在一些重要位置(Sleep Call,Timer Tick)放置了中断信号,通过这个信号通知操作系统调度器(Scheduler)进行进程切换。在抢占式模型中,正在运行的进程可能会被强行挂起,这是由于这些中断信号引发的。
-
协作式(Cooperative)。 协作式调度也叫非抢占式调度,是指当前运行的进程通过自身代码逻辑出让CPU控制权。与抢占式调度的区别在于进程运行不会被中断信号打断,除非其主动出让控制权给其他进程。
-
结构示意图
1.4、协程(Coroutines - Cooperative User-Level Threads)
协程: 又称微线程,纤程。英文名Coroutine。是应用程序通过线程库自行实现的 协作式调度 的运行在用户空间的用户线程,是编译器级别的。系统的并发是时间片的轮转 ,单处理器交互执行不同的执行流,营造不同线程同时执行的感觉;而 协程的并发,是单线程内控制权的轮转 。相比抢占式调度,协程是主动让权,实现协作。
二、并发模型
2.1、、模型:单进(线)程·循环处理请求
单进程和单线程其实没有区别,因为一个进程至少有一个线程。循环处理请求应该是最初级的做法。当大量请求进来时,单线程一个一个处理请求,请求很容易就积压起来,得不到响应。这是无并发的做法。
2.2、模型:多进程(Multiprocessing)
主进程监听和管理连接,当有客户请求的时候,fork 一个子进程来处理连接,父进程继续等待其他客户的请求。
优点:好处是隔离性,子进程万一 crash 并不会影响到父进程。 缺点:缺点就是对系统的负担过重。
典型的是 Apache Web Server,每个用户请求接入的时候都会创建一个进程,这样应用就可以同时支持多个用户。
在图中M1、M2与M3都代表内存资源,在多进程中如果不同进程想共享内存中的数据必须通过 进程间通信的方式来实现。
2.3、模型:多线程(Multithreaded)
在操作系统的视角看,比如Linux中,在进程中创建线程是通过 clone() 系统调用来实现,这和创建子进程的区别不大。线程与进程的区别在于同一个进程内的线程共享着进程分配的资源,线程不被分配资源,只是操作系统调度执行任务的抽象的最小单元。
比如下图中,PID为10的进程P0通过clone()系统调用创建了3个线程,这些线程都可以访问进程分配的内存资源M0。
2.3.1、多线程:通信方式——共享内存通信(Shared memory communication)
共享内存通信(Shared memory communication) :不同线程间可以访问同一内存地址空间,并可修改此地址空间的数据。
同步(Synchronize)访问
因为线程间共享内存资源,所以在访问临界区域时会出现数据竞争。解决竞态条件的方式是对数据进行 同步(Synchronize)访问 。要实现同步访问常见的方式有: * 锁(Lock) :通过锁定临界区域来实现同步访问。 * 信号量(Semaphores) :可以通过信号量的增减控制对一个或多个线程对临界区域的访问。 * 同步屏障(Barriers) :通过设置屏障控制不同线程执行周期实现同步访问。
此模型的优点:
- 大多编程语言都支持此模型;
- 贴近硬件架构,使用得当性能很高;
- 是其他并发模型的基础;
此模型的缺点:
- 不支持分布式内存模型,只解决了进程内的并发同步;
- 不好调试与测试,想用好不容易;
STM(Software transactional memory)
STM是用软件的方式去实现事务内存(Transactional memory),而事务内存中的事务(Transactional)正是关系型数据库中的概念,一个事务必须满足ACID性质,在STM的事务中尽可能避免副作用,比如在事务中去修改原子变量这种操作,可能会导致事务回滚失败。
此模型的优点:
- 相比锁模型更简单;
- 大部分情况下更高效;
此模型的缺点:
- 在事务内需要避免产生副作用;
- 不支持分布式内存模型,只解决了进程内的并发同步;
2.3.2、多线程:通信方式——消息传递通信(Message passing communication)
消息传递通信(Message passing communication) :不同线程间只能通过收发消息的形式去通信,数据只能被拥有它的线程修改。
通信顺序进程(CSP(Communicating sequential processes))
CSP:是一种形式语言,用来描述基于消息传递通信的安全并发模型。各任务模块之间的通信是基于 通道(Channel) 来完成的。通道可以被不同的任务块共享 。通道两端任务块的通信可以是同步的,也可以是异步的。
此模型的优点:
- 相比锁模型更简单;
- 很容易实现高并发;
此模型的缺点:
- 不支持分布式内存模型,只解决了进程内的并发同步;
Actor
演员模型(Actor) 是一种类似面向对象编程思想的安全并发模型。Actor模型=数据+行为+消息。Actor模型内部的状态由自己的行为维护,外部线程不能直接调用对象的行为,必须通过消息才能激发行为,这样就保证Actor内部数据只有被自己修改。
相比CSP模型,Actor模型可以跨节点在 分布式集群中运行。实际上Actor模型的代表Erlang正是天然分布式容错的编程语言。
二者的区别:Actor之间直接通讯,而CSP是通过Channel通讯,在耦合度上两者是有区别的,后者更加松耦合。
此模型的优点:
- 相比锁模型更简单;
- 很容易实现高并发;
- 支持分布式内存模型,能实现跨节点的并发同步;
此模型的缺点:
- 存在信箱满后消息丢失的问题;
2.4、事件驱动模型
事件驱动编程 是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。 Event Loop with Multiplexing:此模型巧妙的利用了系统内核提供的I/O多路复用系统调用,将多个socket连接转换成一个事件队列(event queue),只需要单个线程即可循环处理这个事件队列。当然这个线程是有可能被阻塞或长期占用的,针对这种类型的任务处理可以单独使用一个线程池去做,这样就不会阻塞Event Loop的线程了。
此模型的优点:
- 单线程对系统资源的占用很小;
- 很容易实现高并发;
此模型的缺点:
- 不支持分布式内存模型,只解决了进程内的并发同步;
三、总结
高并发的关键在于实现异步非阻塞,更加高效地利用 CPU,涉及的两大安全难题则是线程安全与内存安全。多线程可以达到非阻塞,但占用资源多,切换开销大。协程用栈的动态增长、用户态的调度来避免多线程的两个问题。事件驱动用单线程的方式,避免了占用太多系统资源,不需要关心线程安全,但无法利用多核。具体要采用哪种模型,还是要看需求。