1. 多进程
1.1. 什么是进程
- 进程(Process),是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位;
- 进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义;
- 进程由程序、数据集合和进程控制块三部分组成: – 程序用于描述进程要完成的功能,是控制进程执行的指令集; – 数据集合是程序在执行时所需要的数据和工作区; – 程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。
1.2. 进程的特点
- 进程是一个实体;
- 对操作系统来说,进程是最小的资源管理单元;
- 进程之间相互独立,不能共享变量,进程内的线程对其他进程不可见;
- 任何进程都可以同其他进程一起并发执行;
- 任何进程都可以同其他进程一起并发执行;
2. 多线程
2.1. 什么是线程
- 线程(Thread),是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位;
- 线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程;
- 线程不拥有系统资源,只有运行必须的一些数据结构,线程与父进程的其它线程共享该进程所拥有的全部资源。
- 线程可以创建和撤消程,从而实现程序的并发执行;
- 一般,线程具有就绪、阻塞和运行三种基本状态。
2.2. 线程的特点
- 对操作系统来说,线程是最小的任务执行单元;
- 线程运行在同一个进程下,线程间可以共享内存空间;
- 线程是一个实体;
- 线程上下文切换比进程更频繁;
2.3. 线程的工作原理
- 根据计算机多核 CPU 处理机制,当线程的数量小于处理器的数量时,线程的并发是真正的并发,不同的线程运行在不同的处理器上;
- 但当线程的数量大于 CPU 处理器的数量时,线程的并发会受到一些阻碍,此时并不是真正的并发,因为此时至少有一个处理器会运行多个线程;
- 在单个处理器运行多个线程时,并发是一种模拟出来的状态,几乎所有的操作系统都是采用时间片轮转的方式抢占调度、轮流执行每一个线程,如我们熟悉的Unix、Linux、Windows及macOS等流行的操作系统;
2.4. 线程的任务调度
- 在一个进程中,当一个线程任务执行几毫秒后,会由操作系统的内核(负责管理各个任务)进行调度,通过硬件的计数器中断处理器,让该线程强制暂停并将该线程的寄存器放入内存中,通过查看线程列表决定接下来执行哪一个线程,并从内存中恢复该线程的寄存器,最后恢复该线程的执行,从而去执行下一个任务。
- 上述过程中,任务执行的那一小段时间叫做时间片,任务正在执行时的状态叫运行状态,被暂停的线程任务状态叫做就绪状态,意为等待下一个属于它的时间片的到来。
- 这种方式保证了每个线程轮流执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。
2.5. 线程与进程的区别
- 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
- 线程上下文切换比进程上下文切换要快得多。
2.6. 多线程实例
-
threading.Thread
import time import threading def test1(): print('task 1 start...') time.sleep(1) print('task 1 end') def test2(): print('task 2 start...') time.sleep(2) print('task 2 end') def test3(): print('task 3 start...') time.sleep(3) print('task 3 end') def test4(): print('task 4 start...') time.sleep(4) print('task 4 end') def test5(): print('task 5 start...') time.sleep(5) print('task 5 end') def test(): tasks = [ threading.Thread(target=test1,), threading.Thread(target=test2,), threading.Thread(target=test3,), threading.Thread(target=test4,), threading.Thread(target=test5,), ] for t in tasks: t.start() for t in tasks: t.join() if __name__ == '__main__': test()
-
ThreadPoolExecutor
import time from concurrent.futures import ThreadPoolExecutor from concurrent.futures import wait, ALL_COMPLETED def test1(): print('task 1 start...') time.sleep(1) print('task 1 end') def test2(): print('task 2 start...') time.sleep(2) print('task 2 end') def test3(): print('task 3 start...') time.sleep(3) print('task 3 end') def test4(): print('task 4 start...') time.sleep(4) print('task 4 end') def test5(): print('task 5 start...') time.sleep(5) print('task 5 end') def test(): thread_pool = ThreadPoolExecutor(max_workers=5) tasks = [] tasks.append(thread_pool.submit(test1)) tasks.append(thread_pool.submit(test2)) tasks.append(thread_pool.submit(test3)) tasks.append(thread_pool.submit(test4)) tasks.append(thread_pool.submit(test5)) wait(tasks, return_when=ALL_COMPLETED) if __name__ == '__main__': test()
3. 协程
3.1. 什么是协程
- 协程,又称微线程,纤程;
- 协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的;
- 基于单线程来实现并发,在一个线程中可以开启很多协程;
- 协程并不是实际存在的实体,本质上是一个线程的调度控制机制;
- 在执行程序的过程中,遇到 IO 操作就冻结当前位置的状态,去执行其他任务,在执行其他任务的过程中,会不断地检测上一个冻结的任务是否 IO 结束,如果 IO 结束了,就继续从冻结的位置开始执行。
3.2 为什么需要协程
- 在传统的J2EE系统中,都是基于每个请求占用一个线程去完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时;
- 如果 遇到很耗时的 I/O 行为,这个时候线程一直处于阻塞状态,则整个系统的吞吐立刻下降,如果线程很多的时候,会存在很多线程处于空闲状态(等待该线程执行完才能执行),造成了资源应用不彻底;
- 协程的目的就是当出现长时间的 I/O 操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除 ContextSwitch 上的开销;
3.3. 协程的工作原理
- 当出现 I/O 阻塞的时候,由协程的调度器进行调度,通过将数据流立刻 yield 掉(主动让出),并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复栈,并把阻塞的结果放到这个线程上去跑;
- 这样看上去好像跟写同步代码没有任何差别,这整个流程可以称为 coroutine,而跑在由 coroutine 负责调度的线程称为 Fiber。由于协程的暂停完全由程序控制,发生在用户层;而线程的阻塞状态是由操作系统内核来进行切换,发生在内核层。因此,协程的开销远远小于线程的开销,也就没有了ContextSwitch上的开销;
- 协程比线程之间的切换和线程的创建销毁所花费的时间,空间开销要小的多;
3.4. 协程的特点
- 协程并不是一个实体;
- 协程在一个线程里实现并发;
- 协程工作在单个线程内,只有挂起、恢复和销毁三种状态
- 用户程序里自己保存多个控制流的上下文栈;
- 一个协程遇到 I/O 操作自动切换到其他协程;
- 线程的切换由操作系统负责调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率;
- 线程的默认 Stack 大小是 1M,而协程更轻量,接近 1K,因此可以在相同的内存中开启更多的协程;
- 协程工作在在同一个线程上,因此可以避免竞争关系而使用锁,因此协程修改共享数据不需要加锁;
- 协程适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好实用线程去解决。
3.5. 协程的有点
- 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级。
- 单线程内就可以实现并发的效果,最大限度地利用cpu.
3.6. 协程的缺点
- 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启多个协程。
- 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程。
4. 协程 & 进程 & 线程 的区别
- 协程,是函数级别的调度;
- 进程 & 线程,都是内核级别的程序调度;
对比维度 | 进程 | 线程 | 协程 |
---|---|---|---|
调度 | 系统内核 | 系统内核 | 用户 |
创建、销毁 | 一次创建、一次销毁 | 多线程频繁创建、切换,耗费 CPU 资源 | 一次创建、一次销毁 |
内存、CPU | 占用内存多、切换复杂,CPU 利用率低 | 占用内存少、CPU 利用率高 | 资源占用小 |
通信 | 多进程 不能 共享变量 | 多线程 可以 共享变量 | 协程可以共享变量 |
同步 | Queue、Pipe | 共享变量 & 锁 | 不需要锁 |
应用 | 多进程适用于 CPU 密集型操作(计算) | 多线程适用于 IO 密集型操作(爬虫) | 协程适用于高并发需求场景,如爬虫并发、服务器并发 |