Python中的进程线程协程
进程
- 可以充分利用多核,进程间通信会耗费额外的开销,达不到多线程应有的效率
线程
- 由于GIL的存在,无法利用多核
- 适合I/O密集型(IO阻塞期间CPU转去执行其他线程)
- 不适合CPU密集型/计算密集型;对于计算密集型任务,开启多线程几乎没有意义,甚至可能比顺序执行用时更长 (顺序执行时,cpu集中完成这个线程的任务,再转去下一个,没有切换,也就没有切换的消耗;开多线程又不是并发,白白增加了切换消耗)
- 进程每次释放GIL锁,所属的线程进行锁竞争、切换线程,会消耗资源
- 线程切换是由操作系统的调度器进行控制的
- 抢占式调度顺序具有不确定性,需要注意处理数据同步
协程
是一种用户级的轻量级的线程,拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器和栈存放在指定地方,在切换回来的时候,恢复先前的上下文和栈
- 用户需要自己编写调度逻辑,控制任务的切换,那么自然可以控制任务的执行顺序
- 实现:yield(原生)/gevent/greenlet
- 比线程消耗更少的资源
- 对cpu来说,协程就是单线程,cpu不用考虑上下文调度和切换的问题,省去了cpu调度的开销,所有协程在一定程度上要优于线程
- 协程相当于在单个进程里启动里一个单线程,同时又可以实现多线程的效果
- 协程本质上是一个线程里面的细分,操作系统层面根本不知道这种区分,只当其是一个线程,程序员自己在应用层面通过yield进行协成间的切换
GIL
GIL,即Global Interpreter Lock全局解释器锁,保证任何时刻仅有一个线程在执行。常见例子有CPython(JPython不使用GIL)与Ruby MRI。 不是所有的Python解释器都有GIL。
为什么要有GIL这个东西,不能去掉呢?这是为了实现GC,避免Java里Full GC(stop the world)引入的。更多内容参考 python 线程,GIL 和 ctypes
每个进程一把锁🔒,进程里的线程,只能有一个线程可以获得这把锁,进行工作,其他线程必须等待
在Python语言的主流实现CPython中,GIL 是一个货真价实的全局线程锁,在解释器解释执行任何Python代码时,都需要先获得这把锁才行, 在遇到I/O操作时会释放这把锁。如果是纯计算的程序,没有I/O操作,解释器会每隔100次操作就释放这把锁,让别的线程有机会执行(这个次数可以通过 sys.setcheckinterval 来调整)
同一进程下的多线程共享数据,共享意味着竞争,竞争带来无序,为了数据安全所以需要加锁进行数据保护,GIL本质是一把互斥锁,使并发变为串行, 保证同一时间只有一条线程访问解释器级别的数据,这样就保证了解释器级别的数据安全,同时也带来了一些问题,同一进程只有一条线程执行任务, 无法利用多核优势,解决方案可以根据任务的类型来处理,如果是I/O密集型,则需要开多线程提高效率,如果是计算密集型则需要多进程。
遇到阻塞,普通单线程的做法是就地等待,多线程的做法是转去执行其他线程–这就是开多线程效率提高的原因。 但如果这种阻塞和放开是我们人为可以预料到的,那么我们可以在阻塞的地方yield返回到另一个可以执行的点,这样也实现了CPU的不浪费,同时还少了线程的切换,效率更高
python锁
- 同步锁(互斥锁)-解决数据安全问题-threading.Lock
- 递归锁-解决线程死锁-threading.R_Lock
- Semaphore信号量-threading.Semaphere
- Event同步事件对象-threading.Event-让两个线程保持同步而不是独立的运行:你给我发个信号,我接到了做一个操作,再给你发一个信号,你接到了一个操作,发下一个信号……
- Python中的多线程通信-queue线程队列