一、进程线程协程的应用场景
CPU密集型
CPU密集型也叫计算密集型,计算密集型任务的特点是要进行大量的计算,消耗CPU资源,CPU占用率接近100%,比如计算圆周率。
IO密集型
IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。
对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,
进程
场景 CPU密集型 利用多核、高计算型的程序、启动数量有限
进程是计算机中最小的资源分配单位
进程和线程是包含关系 每个进程中都至少有一条线程
可以利用多核,数据隔离
创建 销毁 切换 时间开销都比较大
随着开启的数量增加 给操作系统带来负担
线程
IO密集型 调度是我们不能干预的 我们只能写我们自己的逻辑
场景 一些协程现有的模块不能完成帮助我们规避IO操作的功能 适合使用多线程 urllib
被CPU调度的最小单位,线程的切换时操作系统完成的
在cpython解释器下不能利用多核,数据共享
创建 销毁 切换 时间开销都比进程小很多
随着开启的数量增加 给操作系统带来负担
协程
IO密集型 用户可以自己控制的 我们能否抢占更多的资源完全取决于我们切换策略
场景 一些通用的场景 可以用协程现有的模块来规避一些IO操作适合使用协程
协程的切换工作是用户完成的
是一个线程,完全不能利用多核,不会产生数据不安全的现象
多个任务之间互相切换不依赖操作系统,无论开启多少个协程都不会给操作系统带来负担
即:多进程比较适用于CPU密集型的任务,线程和协程比较适用于IO密集型的任务
二、GIL锁与线程互斥锁的理解
GIL锁
GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。
某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。
拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有。
即保证同一时刻只有一个线程能使用到cpu,保证多个线程在修改一个变量时不会崩溃,但结果可能是混乱的。 GIL锁的释放
在python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL) 互斥锁 : 多线程时,保证修改共享数据时有序的修改,不会产生数据修改混乱。 对于GIL和线程的锁,我的理解是:
num = 100 def count():
global num
print(num)
num -= 1 # 线程的资源是共享的 # 需要注意的是,num -= 1的操作是:先读取num,然后修改num的值,最后再写回内存
当没有使用互斥锁,进程中有两个线程(t1, t2)都在执行count函数
1.t1获取到GIL锁,去执行代码,当执行到 print(num) 这里的时候,GIL执行时间达到阈值,t1释放GIL(此时num没有被修改,num=100)
2.t2竞争到了GIL锁,也去执行代码,当执行到 num -= 1 这里的时候,GIL执行时间达到阈值,t2释放GIL(假设此时num在t2这里已经被修改num=99,但是还没来得及把num更新到内存,即别的线程读取num的时候,num还是100)
3.t1再次获取到GIL锁,继续去执行刚才剩下的代码,由于内存中的num还未更新,此时t1执行 num -= 1 num还是等于100-1,然后t1把num=99更新到内存
4.t2获取到GIL锁,继续刚才的操作,把num=99更新到内存。
5.那么此时数据就混乱了,按我们的逻辑,count函数执行了两次,num应该是98,但实际等于99。 6.如果count函数在修改num的时候加了线程的互斥锁,那么即使获取到了GIL,但没有获取到线程互斥锁,仍然无法修改num
7.只有同时获取了GIL锁和互斥锁的线程才能去修改num,线程锁保证修改共享数据时有序的修改,不会产生数据修改混乱 多进程中的GIL
每个进程被fork或spawn时,其实都是开启了一个新的 python解释器进程,所以每个子进程都拥有一个独立的GIL锁。这样的话每个进程里面至少有一个住线程在跑,进程内的线程就实行GIL机制。这样就可以发挥多核优势。