Part 6.系统编程之线程--2(同步,互斥锁,死锁)
(一)同步
通过上一篇的介绍,我们发现了多线程开发可能遇到的问题,问题产⽣的原因就是没有控制多个线程对同⼀资源的访问,对数据造成破坏,使得线程运⾏的结果不可预期,这种现象称为“线程不安全”。
如何解决这个问题,就要引入同步,那什么是同步呢?
同步就是协同步调,按预定的先后次序进⾏运⾏。如:你说完,我再说。
"同"字从字⾯上容易理解为⼀起动作,其实不是,"同"字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B⼀块配合,A执⾏到⼀定程度时要依靠B的某个结果,于是停下来,示意B运⾏; B依⾔执⾏,再将结果给 A; A再继续操作。
所以对于之前提出的那个计算错误的问题,可以通过 线程同步 来进⾏解决,思路如下:
1. 系统调⽤work1,然后获取到num的值为0,此时上⼀把锁,即不允许其他现在操作num
2. 对num的值进⾏+1并更新num
3. 解锁,此时num的值为1,其他的线程就可以使⽤num了,⽽且是num的值不是0⽽是1
4. 同理其他线程在对num进⾏修改时,都要先上锁,处理完后再解锁,在 上锁的整个过程中不允许其他线程访问,就保证了数据的正确性。
(二)互斥锁
什么是互斥锁?
我们知道当多个线程⼏乎同时修改某⼀个共享数据的时候,需要进⾏同步控制;
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引⼊互斥锁;
互斥锁为资源引⼊⼀个状态:锁定/⾮锁定;
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“⾮锁定”,其他的线程才能再次锁定该资源。
互斥锁保证了每次只有⼀个线程进⾏写⼊操作, 从⽽保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以⽅便的处理锁定:
1 from threading import Lock 2 3 mutex = Lock()#创建一个锁对象 4 5 mutex.acquire()#使这把锁上锁 6 7 mutex.release()#解锁
其中,锁定⽅法acquire可以有⼀个blocking参数和timeout参数。
- 如果设定blocking为True,则当前线程会堵塞,直到获取到这个锁为⽌(如果没有指定,那么默认为True)
- 如果设定blocking为False,则当前线程不会堵塞
- 如果没有设定timeout即默认永远阻塞
- 如果设定了timeout参数即会超时后跳过该阻塞区
所以用互斥锁修改代码后如下:
1 from threading import Thread, Lock 2 import time 3 4 num = 0 5 6 7 def work1(): 8 global num 9 #上锁,只要其先上锁,后面的线程就得等着 10 mutex.acquire() 11 for i in range(1000000): 12 num += 1 13 #解锁,供后面的线程使用 14 mutex.release() 15 print("在work1中unm为:%d" % num) 16 17 18 def work2(): 19 global num 20 #前面线程解锁后,他才可以开始上锁 21 mutex.acquire() 22 for i in range(1000000): 23 num += 1 24 #解锁 25 mutex.release() 26 print("在work2中unm为:%d" % num) 27 28 29 if __name__ == "__main__": 30 #创建一把锁 31 mutex = Lock() 32 t1 = Thread(target=work1) 33 t1.start() 34 t2 = Thread(target=work2) 35 t2.start() 36 37 38 》》》输出: 39 在work1中unm为:1000000 40 在work2中unm为:2000000
但是我们想一想,锁放在这个地方真的是最合适的吗?
我们知道此时加上锁,相当于多任务变单任务,即两个线程需要相互等待去抢锁,此时,我们需要知道,锁住的东西应该越少越好,因为这样,才能保证两个线程可以雨露均沾,否则,如上述所示加锁方式,work2线程就得等到work1完全执行完才能开始执行。
所以加锁时的一个原则是,锁住 应该上锁的最小部分。
所以最好应该在for循环里面上锁,锁住 num = num + 1 即可,这样两个线程可以各自竞争上锁,但又保证了num = num + 1 执行过程不会被打断。
小结:
在多线程开发中,全局变量是多个线程都共享的数据,⽽局部变量等是各⾃线程的,是⾮共享的,就算是两个线程运行同一块代码,但是内存会为其各自开辟一块内存,不会相互影响。
(三)死锁
在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时等待对⽅的资源,就会造成死锁。
尽管死锁很少发⽣,但⼀旦发⽣就会造成应⽤的停⽌响应。下⾯看⼀个死锁的例⼦:
1 import threading 2 import time 3 4 5 class MyThread1(threading.Thread): 6 def run(self): 7 if mutexA.acquire(): 8 print(self.name + '--doing-up--') 9 time.sleep(1) 10 11 if mutexB.acquire(): 12 print(self.name + '--doing-down--') 13 mutexB.release() 14 mutexA.release() 15 16 17 class MyThread2(threading.Thread): 18 def run(self): 19 if mutexB.acquire(): 20 print(self.name + '--doing-up--') 21 time.sleep(1) 22 23 if mutexA.acquire(): 24 print(self.name + '--doing-down--') 25 mutexB.release() 26 mutexB.release() 27 28 29 if __name__ == "__main__": 30 mutexA = threading.Lock() 31 mutexB = threading.Lock() 32 t1 = MyThread1() 33 t2 = MyThread2() 34 t1.start() 35 t2.start() 36 37 》》》输出: 38 Thread-1--doing-up-- 39 Thread-2--doing-up-- 40 一直不结束
此时就出现了死锁情况,分析如下:
因此避免死锁的方法就是:添加超时时间。
实际上,在mutex.acquire()中可以添加超时时间,即超过设定时间后,我就不等待了,就直接跳过它继续运行。所以上述代码中如果在第一个里面添加timeout,那在超时时间以后,他就会跳过if语句块,解开A的锁,从而使第二个线程可以执行if语句块。
(四)同步应用----多个线程有序执行
1 import threading 2 import time 3 4 class MyThread1(threading.Thread): 5 def run(self): 6 while True: 7 #由于A没有事先上锁,所以此时满足条件 8 if mutexA.acquire(): 9 print('--Task1--') 10 time.sleep(0.5) 11 #解开B的锁,让B执行 12 mutexB.release() 13 #结束后,又得等待自己的锁被解开 14 15 16 class MyThread2(threading.Thread): 17 def run(self): 18 while True: 19 #由于上面已经把B的锁解开了,此时满足条件运行 20 if mutexB.acquire(): 21 print('--Task2--') 22 time.sleep(0.5) 23 #解开C的锁,让C执行 24 mutexC.release() 25 # 结束后,又得等待自己的锁被解开 26 27 28 class MyThread3(threading.Thread): 29 def run(self): 30 while True: 31 # 由于上面已经把C的锁解开了,此时满足条件运行 32 if mutexC.acquire(): 33 print('--Task3--') 34 time.sleep(0.5) 35 # 解开A的锁,让A执行,从而达到一种按次序运行的效果 36 mutexA.release() 37 # 结束后,又得等待自己的锁被解开 38 39 40 if __name__ == "__main__": 41 mutexA = threading.Lock() 42 #先给B,C上锁 43 mutexB = threading.Lock() 44 mutexB.acquire() 45 mutexC = threading.Lock() 46 mutexC.acquire() 47 48 t1 = MyThread1() 49 t2 = MyThread2() 50 t3 = MyThread3() 51 t1.start() 52 t2.start() 53 t3.start() 54 55 56 》》》输出: 57 --Task1-- 58 --Task2-- 59 --Task3-- 60 --Task1-- 61 --Task2-- 62 --Task3--
可以使⽤互斥锁完成多个任务,有序的进行⼯作,这就是线程的同步!