目录
第十五章、Python多线程同步锁,死锁和递归锁
1. 引子:
1.创建线程对象t1 = threading.Thread(target=say,args=('tony',))2.启动线程t1.start()后面又说了两个点就是join和守护线程的概念
以上就是python多线程的基本使用
说明:前面说的两个功能是相互独立的,相互不干涉的,不会用到同享的资源或者数据,如果我们多个线程要用到相同的数据,那么就会存在资源争用和锁的问题,不管在什么语言中,这个都是不能避免的。 那么接下来讲讲同步锁,死锁和递归锁的使用
2.同步锁
锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁。
适用同步锁的例子如下:
import threadingimport timenum = 100def fun_sub(): global num # num -= 1 num2 = num time.sleep(0.001) num = num2-1if __name__ == '__main__': print('开始测试同步锁 at %s' % time.ctime()) thread_list = [] for thread in range(100): t = threading.Thread(target=fun_sub) t.start() thread_list.append(t) for t in thread_list: t.join() print('num is %d' % num) print('结束测试同步锁 at %s' % time.ctime())-----------------------------------------------------开始测试同步锁 at Sun Apr 28 09:56:45 2019num is 91结束测试同步锁 at Sun Apr 28 09:56:45 2019
这样的例子描述:创建100的线程,然后每个线程去从公共资源num变量去执行减1操作,按照正常情况下面,等到代码执行结束,打印num变量,应该得到的是0,因为100个线程都去执行了一次减1的操作。
这样的问题是:发现结果不是0而是91
让我们整理一下代码思路:
1.因为GIL,只有一个线程(假设线程1)拿到了num这个资源,然后把变量赋值给num2,sleep 0.001秒,这时候num=100
2.当第一个线程sleep 0.001秒这个期间,这个线程会做yield操作,就是把cpu切换给别的线程执行(假设线程2拿到个GIL,获得cpu使用权),线程2也和线程1一样也拿到num,返回赋值给num2,然后sleep,这时候,其实num还是=100. 3.线程2 sleep时候,又要yield操作,假设线程3拿到num,执行上面的操作,其实num有可能还是100 4.等到后面cpu重新切换给线程1,线程2,线程3上执行的时候,他们执行减1操作后,其实等到的num其实都是99,而不是顺序递减的。 5.其他剩余的线程操作如上
解决方案:这里就要借助于python的同步锁了,也就是同一时间只能放一个线程来操作num变量,减1之后,后面的线程操作来操作num变量。看看下面我们怎么实现。
import threadingimport timenum = 100def fun_sub(): global num lock.acquire() print('----加锁----') print('现在操作共享资源的线程名字是:',t.name) num2 = num time.sleep(0.001) num = num2-1 lock.release() print('----释放锁----')if __name__ == '__main__': print('开始测试同步锁 at %s' % time.ctime()) lock = threading.Lock() #创建一把同步锁 thread_list = [] for thread in range(100): t = threading.Thread(target=fun_sub) t.start() thread_list.append(t) for t in thread_list: t.join() print('num is %d' % num) print('结束测试同步锁 at %s' % time.ctime()) ------------------------------------------------ .......----加锁----现在操作共享资源的线程名字是: Thread-98----释放锁--------加锁----现在操作共享资源的线程名字是: Thread-100----释放锁----num is 0结束测试同步锁 at Sun Apr 28 12:08:27 2019
思路:看到上面我们给中间的减1代码块,加个一把同步锁,这样,我们就可以得到我们想要的结果了,这就是同步锁的作用,一次只有一个线程操作同享资源。
3.死锁
引子:
死锁的这个概念在很多地方都存在,比较在数据中,大概介绍下死锁是怎么产生的
# 线程1拿到了(锁头2)想要往下执行需要(锁头1),# 线程2拿到了(锁头1)想要往下执行需要(锁头2)# 互相都拿到了彼此想要往下执行的必需条件,互相都不放手里的锁头.# 产生了死锁问题
产生原因:python中在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。
from threading import Thread,Lockmutex1 = Lock()mutex2 = Lock()import timeclass MyThreada(Thread): def run(self): self.task1() self.task2() def task1(self): mutex1.acquire() print(f'{self.name} 抢到了 锁1 ') mutex2.acquire() print(f'{self.name} 抢到了 锁2 ') mutex2.release() print(f'{self.name} 释放了 锁2 ') mutex1.release() print(f'{self.name} 释放了 锁1 ') def task2(self): mutex2.acquire() print(f'{self.name} 抢到了 锁2 ') time.sleep(1) mutex1.acquire() print(f'{self.name} 抢到了 锁1 ') mutex1.release() print(f'{self.name} 释放了 锁1 ') mutex2.release() print(f'{self.name} 释放了 锁2 ')for i in range(3): t = MyThreada() t.start()
那么,为了解决这个死锁问题,就引入了递归锁方案
4.递归锁RLock
原理:
为了支持在同一线程中多次请求同一资源,python提供了"递归锁":threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源
不多说,放代码
from threading import Thread,Lock,RLock# mutex1 = Lock()# mutex2 = Lock()mutex1 = RLock()mutex2 = mutex1import timeclass MyThreada(Thread): def run(self): self.task1() self.task2() def task1(self): mutex1.acquire() print(f'{self.name} 抢到了 锁1 ') mutex2.acquire() print(f'{self.name} 抢到了 锁2 ') mutex2.release() print(f'{self.name} 释放了 锁2 ') mutex1.release() print(f'{self.name} 释放了 锁1 ') def task2(self): mutex2.acquire() print(f'{self.name} 抢到了 锁2 ') time.sleep(1) mutex1.acquire() print(f'{self.name} 抢到了 锁1 ') mutex1.release() print(f'{self.name} 释放了 锁1 ') mutex2.release() print(f'{self.name} 释放了 锁2 ')for i in range(3): t = MyThreada() t.start()
总结:
上面我们用一把递归锁,就解决了多个同步锁导致的死锁问题。大家可以把RLock理解为大锁中还有小锁,只有等到内部所有的小锁,都没有了,其他的线程才能进入这个公共资源。
5. 大总结
还有一点,并不是所有的多线程都存在数据不同步、死锁的问题,但在访问共享资源的时候,锁是一定要存在了, 所以我们在代码里面加锁的时候,要注意在什么地方加,对性能的影响最小,这个就靠对逻辑的理解了。