线程锁(互斥锁Mutex)
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果2个线程同时要修改同一份数据,会出现什么状况?
# -*- coding:utf-8 -*- import threading import time num = 100#设定一个共享变量:num=100在主线程中,我想要在子线程中修改num def run(n): global num#在函数里修改函数外变量,首先应该声明为全局变量,在每个线程中都获取这个全局变量 time.sleep(1)#线程运行过程中遇到耗时操作,则解释器锁解开,使其他线程运行,即:此时不占用cpu,直接使这个线程挂起,cpu去干别的去了。 num -= 1#对此公共变量进行-1操作 t_objs = []#存线程实例 for i in range(100): t = threading.Thread(target=run, args=("t-%s"%i,)) t.start() t_objs.append(t) for t in t_objs: t.join()#不加join,直接就运行出的结果是个中间状态。 print('final num:', num ) #结果:('final num:', 4)
正常来讲,这个num结果应该是0, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给cpu去运算,当A线程去处完的结果是99,但此时B线程运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。
*注:不要在3.x上运行,不知为什么,3.x上的结果总是正确的,可能是自动加了锁
加锁版本
# -*- coding:utf-8 -*- import threading import time num = 100 def run(n): global num time.sleep(1) lock.acquire() # 修改数据前加锁 num -= 1 #使中间过程变成串行 lock.release() # 修改后释放 lock = threading.Lock()#生成全局锁 t_objs = [] for i in range(100): t = threading.Thread(target=run, args=("t-%s"%i,)) t.start() t_objs.append(t) for t in t_objs: t.join() print('final num:', num ) #结果:('final num:', 0)
GIL VS Lock
机智的同学可能会问到这个问题,就是既然你之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 注意啦,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下+配合我现场讲给大家,就明白了。
那你又问了, 既然用户程序已经自己有锁了,那为什么C python还需要GIL呢?加入GIL主要的原因是为了降低程序的开发的复杂度,比如现在的你写python不需要关心内存回收的问题,因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题, 这可以说是Python早期版本的遗留问题。