了解进程、线程及协程对前端同学了解 IO 这块帮助极大,对从事 Node 开发的同学更是如此。
1、进程
进程是一种古老而典型的上下文系统,每个进程有独立的地址空间,资源句柄,他们互相之间不发生干扰。
每个进程在内核中会有一个数据结构进行描述,我们称其为进程描述符。这些描述符包含了系统管理进程所需的信息,并且放在一个叫做任务队列的队列里面。
2、进程状态
我们实际上可以把进程状态归结为三个最主要的状态:就绪态,运行态,阻塞(等待)态。这是任何一本系统书上都有的三态转换图。 就绪和执行可以互相转换,基本这是调试的过程。而当执行态程序需要等待某些条件(最典型就是IO)时,就会陷入睡眠态。而条件达成后,一般会自动进入就绪。
3、线程
线程是一种轻量进程,实际上在 Linux 内核中,两者几乎没有差别,除了一点——线程并不产生新的地址空间和资源描述符表,而是复用父进程的。
4、协程
有以下这种情况,不断重复网络 IO,然后计算
┌────────────┐ ┌──────────────────────┐
│http request│────▶│ Mass computation │
└────────────┘ └──────────────────────┘
▲ │
└────┐ └────┐
│ ▼
┌──────────────────────┐ ┌────────────┐
│ Mass computation │◀────│http request│
└──────────────────────┘ └────────────┘
协程简单的讲就是在单线程程序遇到 IO 时,由系统处理 IO,线程接下来的计算继续由 CPU 处理。如果用多线程处理上面的情况,往往是单个线程等待 IO,然后另起一个线程由 CPU 继续计算,等 IO 结束,再将上下文切回去,多进程同理。所以,利用协程可以避免线程或者进程切换带来的开销
讲到协程,就难免会想到 JS 的异步,其实 JS 中的异步就是协程。在 JS 执行线程中进行的行为被称作同步 ( Synchronous ) 操作,非 JS 执行线程执行的行为则被称为异步 ( Asynchronous ) 操作。
这里来段 Python 的例子,不用 JS 是因为 Python 写的更有感觉 :)
import asyncio
async def log_task():
s = 1 + 2 # 仿计算密集型
print('>>>>>>>>>log begin n sum', s)
r = await asyncio.sleep(5) # 仿IO密集型
print('>>>>>>log end')
task = [log_task(), log_task(), log_task()]
asyncio.get_event_loop().run_until_complete(asyncio.wait(task))
#-------- 分割线-------------
# 先由cpu处理
>>>>>>>>>log begin
sum 3
>>>>>>>>>log begin
sum 3
>>>>>>>>>log begin
sum 3
# 等待 5s,系统io结束
>>>>>>log end
>>>>>>log end
>>>>>>log end
当协程发出 IO 指令,会将其状态变更为等待。此时,主线程继续同步执行 Task,待 CPU 计算执行完毕,Evetloop 线程唤起执行 IO。请记住,这里的执行是并发不是并行(并发,不同代码块交替执行(coroutine);并行,不同代码块同时执行)
5、并发与并行
既然讲了,进程、线程这些,并发也并行顺带也讲讲。
一个任务还没执行完,其他任务就开始执行,就是并发。一个任务执行的同时,其他任务同时也在执行,就是并行
并行指物理上同时执行,并发指能够让多个任务在逻辑上交织执行的程序设计。补充其他几个概念:
协程属于并发
大部份语言多进程或者多线程都可以并行
Python 因为 GIL 的问题,多线程只能利用单核,不能并行,只能并发。如果想要并行,需要使用多进程。
6、Python 中的多线程以及GIL
要达到并行状态,最简单的就是利用多线程或多进程。但是 Python 的多线程由于存在著名的 GIL,无法让两个线程真正 “同时运行”。例如当一个线程遇到 IO 时,线程挂起,执行权交到另一个线程上,但无论多少线程都只能使用一个核,所以实际上是无法到达并行状态的。
但 Python 多线程是可以并发的,爬虫存在的网络瓶颈对 Python 多线程来讲不存在,不过 Python 爬虫一般考虑使用协程了。关于 GIL,它主要是语言的实现里面,具体的讲,是 C 语言版的 Python。其他语言,即使用了多线程,如果状态同步没有采用 GIL 去做,是不会有 GIL 的问题的。
如果服务器是单核的,多进程没用,只会增进程开销,直接上多线程或者协程。
之前对某个 OA 系统进行过爬取,地址在这里
7、Tips
先知道你的瓶颈在哪里再做相应的改动,而不是凭感觉去觉得哪里有问题就改哪里,可以看看阿布达尔法则
如何利用多核 CPU? 像 Node、Python 这样的语言,如果要多核,只能多进程,Golang 则可以多线程多核。
如果有带宽瓶颈,直接上协程就好;如果是高并发,不计成本,可以用多进程&协程,并行加并发处理资源。