一、什么是Cpython GIL
GIL Global inteperter lock
Cpython 解释器的内存管理并不是线程安全的,为了保护多个线程对python对象的访问,cpython 使用了简单的锁机制来避免多个线程同时执行字节码 【python执行是先把源文件编译成字节码然后再执行的】
二、单核CPU的线程调度方式
单核CPU上调度多个线程任务,大家相互共享一个全局锁,谁在CPU执行,谁就占有这把锁,直到这个线程因为IO操作或者Timer Tick到期让出CPU,没有在执行的线程就安静的等待着这把锁(除了等待之外,他们应该也无事可做)。下面这个图演示了一个单核CPU的线程调度方式:
三、GIL 影响
限制了程序的多核执行
同一个时间只能有一个线程执行字节码
CPU密集程序难以利用多核优势
IO期间会释放GIL,对IO密集程序影响不大
四、规避GIL影响
区分CPU和IO密集型程序
CPU密集可以使用多进程+进程池
IO密集使用 多线程/协程
cython扩展
五、GIL的实现
执行指定数量的字节码之后就会判断是否有全局解释器锁,如果存在则释放,不存在则等待
说白了就是 每隔一段时间释放当前的全局解释器锁,让其他的线程获取到解释器锁并执行,然后再次获取
六、为什么有了GIL还要关注线程安全
6.1 非原子操作不是线程安全的
import threading
import dis #分析字节码的包
n = [0]
def foo():
n[0] = n[0]+1
n[0] = n[0]+1
print(dis.dis(foo))
threads = []
for i in range(5000):
t = threading.Thread(target=foo)
threads.append(t)
print(threads)
for t in threads:
t.start()
print(n)
输出结果:
有可能是10000也有可能小于10000,原因是在进行 n[0] = n[0] + 1 的时候还没有执行完就被其他进程抢走了GIL,GIL的切换原理是执行指定数量的字节码之后或者执行IO操作就进行切换,所以非原子操作线程并安全, 我们可以通过加锁来保证线程安全
6.2 通过加锁保证线程安全
import threading
import dis #分析字节码的包
n = [0]
lock = threading.lock()
def foo():
with lock:
n[0] = n[0]+1
n[0] = n[0]+1
print(dis.dis(foo))
threads = []
for i in range(5000):
t = threading.Thread(target=foo)
threads.append(t)
print(threads)
for t in threads:
t.start()
print(n)
七、服务端性能优化措施
1、业务代码层:业务端代码优化,数据结构和算法优化
2、数据库层(后端和数据库打交道比较多,往往数据库容易成为瓶颈): 增加索引,满查询优化,通过批量操作减少请求服务器次数,引入NoSQL(非结构化的数据)
3、缓存层:增加redis、memcache
4、网络:通过 批量操作,通过pinpline 技术减少请求服务器次数
4、异步任务:引入asyncio celery
5、多进程 :gevent/多进层