什么是GIL
''' 定义: In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.) '''
可以从上面一段关于GIL的定义中总结出三个方面
1.对象是Cpython
GIL并不是python的特性,他只是Cpython解释器为解决某一问题而引入的,像Jpython就没有GIL
2.为什么要有GIL
因为Cpython的内存管理不是线程安全的
3.GIL带来的影响
GIL(全局解释器锁)本质是一个互斥锁,它阻止了同一进程下多个线程同一时刻执行python字节码
Cpython的内存管理为什么不是线程安全的
知识储备
1.python的内存管理说的是,python的垃圾回收机制。对于一个进程来说,每个进程下都有一个专门负责垃圾回收的线程,该线程是解释器级别的,并与其它线程并发运行
2.每个对象上都有两个头部信息:类型标志符与引用计数,而python会自动回收那些引用计数为0的对象
3.执行x=1,实际上是开辟一个内存空间将1这个对象放入,然后让x这个变量名指向这个内存空间(变量的引用)
4.一个.py文件仅仅是一个文本文件,代码的执行是需要python解释器的
假设没有GIL的存在,对于垃圾回收线程和其它线程来说可以是并行的,当线程1执行x=(1,2,3)时,先开辟了内存空间,然后将(1,2,3)这个对象放入,这时候垃圾回收线程有可能恰巧检测到这个对象的引用为0,它就会回收,这就造成了数据的不安全
而GIL的存在就是给解释器加了锁,谁被分配到这把锁谁就能执行(谁就能将所要执行的代码作为参数传给python解释器)
GIL带来的影响
有了GIL的存在意味着在一个进程内,多个线程是无法同时运行的(无法实现并行),也就说同一个进程的多个线程无法利用多核优势,但是这并不能说Cpython的线程就是一个鸡肋
什么是多核优势
多核:多个CPU(由运算器与控制器组成)--->用来完成计算任务/无法进行IO
于单核来说不存在多核优势这一说法
于多核,分两种情况‘
对于IO密集型的任务,多核是没有意义的,相反多线程的效率更高(开启线程的消耗远小于进程)
import time,os from multiprocessing import Process from threading import Thread def foo(name): print('%s正在进行IO...'%name) time.sleep(3) if __name__ == '__main__': start = time.time() l = [] for i in range(4): # p = Process(target=foo,args=('进程%s'%i,)) p = Thread(target=foo,args=('线程%s'%i,)) l.append(p) p.start() for p in l: p.join() end = time.time() print('花费时间:', end - start) print('cpu核数:',os.cpu_count()) # 多进程运行结果: 进程1正在进行IO... 进程0正在进行IO... 进程2正在进行IO... 进程3正在进行IO... 花费时间: 3.2010157108306885 cpu核数: 4 # 多线程运行结果 线程0正在进行IO... 线程1正在进行IO... 线程2正在进行IO... 线程3正在进行IO... 花费时间: 3.0021512508392334 cpu核数: 4
可以从上面看出对于IO密集型任务来说,多进程的花费时间是远大于线程的(而且这种相差,会随着进程数的增大而更加明显)
对于计算密集型的任务,多进程的优势就体现出来了
import time,os from multiprocessing import Process from threading import Thread def task(name): num = 0 print("%s正在计算..."%name) for i in range(100000000): num *= i if __name__ == '__main__': start = time.time() p_list = [] for i in range(4): p = Process(target=task,args=('进程%s'%i,)) # p = Thread(target=task,args=('线程%s'%i,)) p_list.append(p) p.start() for p in p_list: p.join() end = time.time() print('花费时间', end - start) print('cpu核数:',os.cpu_count()) # 多进程运行结果 进程2正在计算... 进程3正在计算... 进程1正在计算... 进程0正在计算... 花费时间 18.54850435256958 cpu核数: 4 # 多线程运行结果 线程0正在计算... 线程1正在计算... 线程2正在计算... 线程3正在计算... 花费时间 41.12251615524292 cpu核数: 4
总结
1.对于IO密集型的任务:使用多线程
对于计算密集型的任务:使用多进程
2.虽然同一个进程下的多个线程无法实现并行,但是不影响并发
GIL与Lock
GIL的存在是为了保护解释器级别的数据安全(垃圾回收),而代码执行的共享数据是需要自定义的互斥锁
import time from threading import Thread,Lock x = 100 def foo(): global x with L: temp = x-1 time.sleep(0.1) x = temp l = [] L = Lock() for i in range(100): t = Thread(target=foo) l.append(t) t.start() for t in l: t.join() print(x) # 没有互斥锁的情况下 执行结果 x = 99 # 加了互斥锁的情况下 执行结果 x = 0
分析
第一个起来的线程-----被分配到GIL(相当于获得调用python解释器的权限)------>执行了global,又拿到了互斥锁L,再执行,当遇到sleep(模拟IO行为),发生阻塞------->被操作系统剥夺CPU,被迫释放GIL(注意,互斥锁L并没有释放),其余在等待获得GIL的线程开始争抢(其实是操作系统分配),但是被阻塞在了获得互斥锁L这里,待第一个线程结束IO行为,与其它线程参与到争抢GIL之中(也只有第一个线程拿到GIL才有意义)