一、什么是Cpython GIL

GIL Global inteperter lock

Cpython 解释器的内存管理并不是线程安全的,为了保护多个线程对python对象的访问,cpython 使用了简单的锁机制来避免多个线程同时执行字节码 【python执行是先把源文件编译成字节码然后再执行的】

二、单核CPU的线程调度方式

单核CPU上调度多个线程任务,大家相互共享一个全局锁,谁在CPU执行,谁就占有这把锁,直到这个线程因为IO操作或者Timer Tick到期让出CPU,没有在执行的线程就安静的等待着这把锁(除了等待之外,他们应该也无事可做)。下面这个图演示了一个单核CPU的线程调度方式:

python 全局 Queue python 全局锁 会降低性能么_python 全局 Queue

三、GIL 影响

限制了程序的多核执行

同一个时间只能有一个线程执行字节码

CPU密集程序难以利用多核优势

IO期间会释放GIL,对IO密集程序影响不大

四、规避GIL影响

区分CPU和IO密集型程序

CPU密集可以使用多进程+进程池

IO密集使用 多线程/协程

cython扩展

五、GIL的实现

执行指定数量的字节码之后就会判断是否有全局解释器锁,如果存在则释放,不存在则等待

说白了就是 每隔一段时间释放当前的全局解释器锁,让其他的线程获取到解释器锁并执行,然后再次获取

python 全局 Queue python 全局锁 会降低性能么_python 全局 Queue_02

 

六、为什么有了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)

python 全局 Queue python 全局锁 会降低性能么_字节码_03

输出结果:

有可能是10000也有可能小于10000,原因是在进行 n[0] = n[0] + 1 的时候还没有执行完就被其他进程抢走了GIL,GIL的切换原理是执行指定数量的字节码之后或者执行IO操作就进行切换,所以非原子操作线程并安全, 我们可以通过加锁来保证线程安全

python 全局 Queue python 全局锁 会降低性能么_python_04

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)

python 全局 Queue python 全局锁 会降低性能么_python_05

 

七、服务端性能优化措施

1、业务代码层:业务端代码优化,数据结构和算法优化

2、数据库层(后端和数据库打交道比较多,往往数据库容易成为瓶颈): 增加索引,满查询优化,通过批量操作减少请求服务器次数,引入NoSQL(非结构化的数据)

3、缓存层:增加redis、memcache

4、网络:通过 批量操作,通过pinpline 技术减少请求服务器次数

4、异步任务:引入asyncio  celery

5、多进程 :gevent/多进层