1. 进程
进程是系统资源分配的最小单位。系统由一个个程序,也就是进程组成的,一般情况下,分为文本区域、数据区域和堆栈区域。
文本区域存储处理器执行的代码(机器码),通常来说,这是一个只读区域,防止运行的程序被意外修改。
数据区域存储所有的变量和动态分配的内存,又细分为初始化的数据区(所有初始化的全局、静态、常量,以及外部变量)和为初始化的数据区(初始化为0的全局变量和静态变量),初始化的变量最初保存在文本区,程序启动后被拷贝到初始化的数据区。
堆栈区域存储着活动过程调用的指令和本地变量,在地址空间里,栈区紧连着堆区,他们的增长方向相反,内存是线性的,所以我们代码放在低地址的地方,由低向高增长,栈区大小不可预测,随开随用,因此放在高地址的地方,由高向低增长。当堆和栈指针重合的时候,意味着内存耗尽,造成内存溢出。
进程的创建和销毁都是相对于系统资源,非常消耗资源,是一种比较昂贵的操作。进程为了自身能得到运行,必须要抢占式的争夺CPU。对于单核CPU来说,在同一时间只能执行一个进程的代码,所以在单核CPU上实现多进程,是通过CPU快速的切换不同进程,看上去就像是多个进程在同时进行。
由于进程间是隔离的,各自拥有自己的内存内存资源,相比于线程的共同共享内存来说,相对安全,不同进程之间的数据只能通过 IPC(Inter-Process Communication) 进行通信共享。
2. 线程
线程是CPU调度的最小单位。如果进程是一个容器,线程就是运行在容器里面的程序,线程是属于进程的,同个进程的多个线程共享进程的内存地址空间。
线程间的通信可以直接通过全局变量进行通信,所以相对来说,线程间通信是不太安全的,因此引入了各种锁的场景,不在这里阐述。
当一个线程崩溃了,会导致整个进程也崩溃了,即其他线程也挂了, 但多进程而不会,一个进程挂了,另一个进程依然照样运行。
在多核操作系统中,默认进程内只有一个线程,所以对多进程的处理就像是一个进程一个核心。
3. 同步和异步
同步和异步关注的是消息通信机制,所谓同步,就是在发出一个函数调用时,在没有得到结果之前,该调用不会返回。一旦调用返回,就立即得到执行的返回值,即调用者主动等待调用结果。所谓异步,就是在请求发出去后,这个调用就立即返回,没有返回结果,通过回调等方式告知该调用的实际结果。
同步的请求,需要主动读写数据,并且等待结果;异步的请求,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
4. 阻塞和非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。所以,区分的条件在于,进程/线程要访问的数据是否就绪,进程/线程是否需要等待。
非阻塞一般通过多路复用实现,多路复用有 select、poll、epoll几种实现方式。
5. 协程
协程是属于线程的,又称微线程,纤程,英文名Coroutine。举个例子,在执行函数A时,我希望随时中断去执行函数B,然后中断B的执行,切换回来执行A。这就是协程的作用,由调用者自由切换。这个切换过程并不是等同于函数调用,因为它没有调用语句。执行方式与多线程类似,但是协程只有一个线程执行。
协程的优点是执行效率非常高,因为协程的切换由程序自身控制,不需要切换线程,即没有切换线程的开销。同时,由于只有一个线程,不存在冲突问题,不需要依赖锁(加锁与释放锁存在很多资源消耗)。
协程主要的使用场景在于处理IO密集型程序,解决效率问题,不适用于CPU密集型程序的处理。然而实际场景中这两种场景非常多,如果要充分发挥CPU利用率,可以结合多进程+协程的方式。后续我们会讲到结合点。
5.1 应用场景
外部接收一些文件,每个文件里有一组数据,其中,这组数据需要通过http的方式,发向第三方平台,并获得结果。由于同一个文件的每一组数据没有前后的处理逻辑,在之前通过Requests库发送的网络请求,串行执行,下一组数据的发送需要等待上一组数据的返回,显得整个文件的处理时间长,这种请求方式,完全可以由协程来实现。
Asyncio是一个异步编程的框架,可以解决异步编程,协程调度问题,线程问题,是整个异步IO的解决方案。为了更方便的配合协程发请求,使用aiohttp库来代替requests库
程序提速的方法
Python对并发编程的支持
- 多线程:
threading
,利用CPU和IO可以同时执行(并行执行)的原理,让CPU不会干巴巴等待IO完成
- 多进程:
multiprocessing
,利用多核CPU的能力,真正的并行执行任务
- 异步IO:
asyncio
,在单线程利用CPU和IO同时执行的原理,实现函数异步执行
- 使用
Lock
对共享资源进行加锁,防止冲突访问e.g. 多线程和多进程同时访问同一个文件,同时写入的话就会出现冲突,我们把文件锁起来就可以实现有序访问
- 使用
Queue
实现不同线程/进程之间的数据通信,实现生产者-消费者模式
- 使用
线程池Pool/进程池Pool
,简化线程/进程的任务提交、等待结束、获取结果
- 使用
subprocess
启动外部程序的进程,并进行输入输出交互e.g. 写好了一个.exe
程序,使用这个模块我们就可以调取这个.exe
程序并和它进行输入输出交互,实现交互式的进程通信。