网络通信与并发编程(七)GIL、协程
GIL、协程
文章目录
- GIL、协程
- 一、GIL
- 二、协程
一、GIL
GIL本质就是CPython解释器中的一把互斥锁。那既然是互斥锁,原理都一样,都是让多个并发线程同一时间只能有一个执行,以此确保共享数据的安全性。有了GIL的存在,同一进程内的多个线程同一时刻只能有一个在运行,意味着在Cpython中一个进程下的多个线程无法实现并行。这也意味着CPython中是无法利用cpu的多核优势的。
简单来讲GIL可以被比喻成执行权限,同一进程下的所有线程要想执行都需要先抢执行权限。
注:CPython是python解释器实现的一种方式,另外还有JPython、PyPy、Psyco等等,只有CPython才有GIL锁,并且大部分环境下默认的python解释器就是CPython。
首先我们需要明确一点,当我们运行py文件时,不光会产生一个主进程(主线程)以及子线程来执行这个py文件的代码,同时解释器也会产生一些解释器级别的线程去完成相关的任务(例如垃圾回收线程)。而解释器中的数据和py文件中的数据又是共享的,所以必须要一种锁来确保多个线程在修改数据时数据的安全性,这就是GIL锁存在的原因。
下面举个具体的例子:假如我们执行py文件生成了一个主进程,同时主进程又产生了三个子线程,则python的执行过程大致如下图所示。
首先主进程、子线程和垃圾回收机制线程会抢GIL锁,只有抢到GIL锁的线程才能执行内部代码并有权修改解释器的数据。需要注意的是子线程也是要抢GIL锁后才能执行的,如果子线程抢到GIL锁后发现需要修改的数据被mutex锁上了,子线程会进入阻塞状态,然后解释器会回收子线程的GIL锁。
问题:既然有了GIL为什么还要设置自定义的互斥锁呢?
GIL锁的是解释器的数据,自定义互斥锁锁的是用户的数据。例如用户创建三个线程修改文件中的某个数据,文件中的数据属于用户自己的数据不归解释器管,所以想要文件数据能被安全的修改必须加上自定义的互斥锁。
由于CPython的多线程无法使用cpu的多核优势问题,这在里再次强调一些python中多线程和多进程的选择问题:
- (cpu多核情况下)若执行的任务是cup密集型的(大量计算任务),则使用多进程。因为python中的多线程无法使用多核cpu的优势,为了充分发挥多核cpu的计算力则需要使用多进程。
- 若执行任务是I/O密集型的(如网络通信等等),则使用多线程。I/O密集型任务涉及大量的I/O操作,而多个进程间的来回切换对比于多个线程之间而言消耗更多时间与资源。
GIL的释放和自定义互斥锁有何不同?
GIL相当于执行权限,会在遇到I/O或者线程运行时间过长时被解释器回收。自定义互斥锁需要手动释放。
二、协程
协程也就是单线程实现并发效果,其本质是在应用程序里控制多个任务的切换并保存状态。
协程的优点:应用程序级别速度要远远高于操作系统的切换(开进程和线程都需要向操作系统申请)
协程的缺点:多个任务一旦有一个阻塞没有切换,整个线程都阻塞在原地,线程内的其他的任务都不能执行了。
协程的实现:为了实现单线程的并发效果,必须要时时检测线程内的I/O操作,遇到I/O操作立马切换其他任务以免阻塞影响运行效率。协程的难点也就在如何让程序在用户层面(不依靠操作系统)就能检测代码中的I/O操作。
问题:cpu并发运行时出现遇到I/O操作或者单个任务运行时间过长/遇到优先级更高的任务这两种情况都会进行切换,为什么协程遇到后者不切换呢?
cpu后面这种情况的切换只是为了让所以的任务都能雨露均沾。当任务为计算型时,后面的切换会降低任务的效率。所以协程只有遇到I/O时才进行切换。
为了实现程序内对I/O操作的检测,需要使用gevent模块,可以使用pip install gevent命令下载该模块。
#monkey.patch_all()为一个补丁,打上该补丁后gevent可以检测非gevent内置的I/O操作
#monkey.patch_all()需要放在最前面
from gevent import monkey,spawn;monkey.patch_all()
#gevent模块中spawn开启的是假线程,可以用current_thread查看假线程的名称
from threading import current_thread
import timedef eat():print('%s eat 1' %current_thread().name)#模拟I/O操作time.sleep(3)print('%s eat 2' %current_thread().name)def play():print('%s play 1' %current_thread().name)#模拟I/O操作time.sleep(1)print('%s play 2' %current_thread().name)#开启协程,若传入的函数有参数,需在spawn函数中传入
#如spawn(eat,1,2,t=3)
g1=spawn(eat)
g2=spawn(play)print(current_thread().name)
#等待协程运行完毕,不写的话主进程代码结束协程的代码会被强制结束
#下方的代码可以简写为gevent.joinall([g1,g2])
g1.join()
g2.join()
#如何传入的函数有返回值,可以通过g1.value得到返回值
用协程实现socket并发:(服务端版)
from gevent import monkey,spawn;monkey.patch_all()
from socket import *
import geventdef talk(conn,addr):try:while True:res=conn.recv(1024)print('client %s:%s'%(addr[0],addr[1]))conn.send(res.upper())except:conn.close()if __name__=='__main__':s=socket(AF_INET,SOCK_STREAM)s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)s.bind(('127.0.0.1',8080))s.listen(5)while True:conn,addr=s.accept()gevent.spawn(talk,conn,addr)