day9-为什么需要线程锁(互斥锁)
一、概述
线程需要沟通,需要共享数据,但是我们之前并没有涉及到多线程情况共享数据的例子。下面我们就来探讨一下,多线程共享数据会出现什么情况。这边就需要用到线程锁,又叫互斥锁(mutex)。
二、线程锁(互斥锁)
2.1、前戏
说明:我们现在来探讨多线程数据共享的情况
import threading,time def run(n): global num #把num变成全局变量 time.sleep(1) #注意了sleep的时候是不占有cpu的,这个时候cpu直接把这个线程挂起了,此时cpu去干别的事情去了 num += 1 #所有的线程都做+1操作 num = 0 #初始化num为0 t_obj = list() for i in range(100): t = threading.Thread(target=run,args=("t-{0}".format(i),)) t.start() t_obj.append(t) for t in t_obj: t.join() print("--------all thead has finished") print("num:",num) #输出最后的num值 #输出 --------all thead has finished ('num:', 97) #输出的结果
这个时候有些小伙伴就说了,你最后输出的结果怎么会是 97 呢?应该是100才对啊,不是有GIL(全局解释器锁)已经控制了,这个到底是为什么呐?
答:注意了,这种情况只能在python2.x 中才会出现的,python3.x里面没有这种现象。下面我们就用一张图来解释一下这个原因。如图:
图解释:
- 到第5步的时候,可能这个时候python正好切换了一次GIL(据说python2.7中,每100条指令会切换一次GIL),执行的时间到了,被要求释放GIL,这个时候thead 1的count=0并没有得到执行,而是挂起状态,count=0这个上下文关系被存到寄存器中.
- 然后到第6步,这个时候thead 2开始执行,然后就变成了count = 1,返回给count,这个时候count=1.
- 然后再回到thead 1,这个时候由于上下文关系,thead 1拿到的寄存器中的count = 0,经过计算,得到count = 1,经过第13步的操作就覆盖了原来的count = 1的值,所以这个时候count依然是count = 1,所以这个数据并没有保护起来.
2.2、添加线程锁
说明:通过上面的图我们知道,结果依然是不准确的。所以我还要加一把锁,这个是用户级别的锁。
import threading,time def run(n): lock.acquire() #获取一把锁 global num time.sleep(0.1) num += 1 lock.release() #释放锁 num = 0 lock = threading.Lock() #添加一个锁的实例 t_obj = list() for i in range(100): t = threading.Thread(target=run,args=("t-{0}".format(i),)) t.start() t_obj.append(t) for t in t_obj: t.join() #因为join是等子线程执行的结果,如果不加,下面可能没有执行完就获取到num的值了 print("--------all thead has finished") print("num:",num) #获取num的结果 #输出 --------all thead has finished ('num:', 100)
小结:
- 用theading.Lock()创建一个lock的实例。
- 在线程启动之前通过lock.acquire()加加锁,在线程结束之后通过lock.release()释放锁。
- 这层锁是用户开的锁,就是我们用户程序的锁。跟我们这个GIL没有关系,但是它把这个数据相当于copy了两份,所以在这里加锁,以确保同一时间只有一个线程,真真正正的修改这个数据,所以这里的锁跟GIL没有关系,你理解就是自己的锁。
- 加锁,说明此时我来去修改这个数据,其他人都不能动。然后修改完了,要把这把锁释放。这样的话就把程序编程串行了。
三、使用场景
在用户层面加锁,使程序变成串行了,那我们在什么情况下用呢?
1、我们在程序中间不能有sleep,因为程序变成串行,这样你再sleep,程序执行的时间就会变长。
2、我们使用的时候确保数据量不是特别大,如果数据量大,也会影响我们的执行效率。
3、如果你程序结束时,不释放锁的话,而且程序又是串行的,则就是占着坑,那永远在那边等着,所以最后需要释放锁。