一、什么是互斥锁
由于并发状态下,操作系统对多个进程进行调度,而多个进程可能都有操作硬件的需求,这时就会产生多个进程对资源的共享,而共享意味着竞争,竞争会产生许多问题。这样就需要一种机制或者手段去解决竞争,使竞争变得有序化,从而使共享资源按照预定的结果去获取。这种手段就是加互斥锁。
使用互斥锁会由并发改为串行,牺牲了运行效率,但保证了数据安全。
二、代码示例
在并发状态下,如果没有互斥锁,多个进程竞争同一硬件,会出现错乱。这就好比多个人合租同一套房,会出现争抢卫生间的现象。一个人上卫生间后上一把锁,其他人都要等着,等到完成这个任务后释放锁。如果没有互斥锁,会产生下列现象:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 from multiprocessing import Process 2 import time 3 import os 4 5 6 def task(name): 7 print('%s 上厕所 [%s]' % (name, os.getpid())) 8 time.sleep(1) 9 print('%s 上完厕所 [%s]' % (name, os.getpid())) 10 11 12 if __name__ == '__main__': 13 for i in range(3): 14 p = Process(target=task, args=('进程%s' % i,)) 15 p.start()
运行结果如下:
这种现象是不应该发生的。可以引入Lock加上互斥锁。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 from multiprocessing import Process, Lock 2 import time 3 import os 4 5 6 def task(name, mutex): 7 mutex.acquire() #落锁 8 print('%s 上厕所 [%s]' % (name, os.getpid())) 9 time.sleep(1) 10 print('%s 上完厕所 [%s]' % (name, os.getpid())) 11 mutex.release() #释放锁 12 13 14 if __name__ == '__main__': 15 mutex = Lock() #父进程创建该变量后,子进程也会拷贝该数据 16 for i in range(3): 17 p = Process(target=task, args=('进程%s' % i, mutex)) #将lock传给子进程,所有子进程用同一把锁 18 p.start()
运行结果如下:
可以发现,由并发改为串行,虽然降低了效率,但是保证了数据安全不错乱。
三、模拟抢票练习
购买火车票的场景比较典型。多个用户可以同时查到的实时余票信息,但购买火车票时是有先后顺序的。下面用文件db.txt当数据库,用多个进程模拟多个人执行抢票任务。
db.txt
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 {"count":1}
模拟当前仅剩一张余票,多个用户同时看到,但只能有一个人抢到
模拟抢票.py
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 from multiprocessing import Process,Lock 2 import time,json 3 4 5 def search(name): 6 time.sleep(1) #模拟网络延迟 7 dic = json.load(open("db.txt","r",encoding="utf-8")) 8 9 print("<%s> 查看剩余票数[%s]" % (name, dic["count"])) 10 11 12 def get(name): 13 time.sleep(1) #模拟网络延迟 14 dic = json.load(open("db.txt", "r", encoding="utf-8")) 15 if dic["count"] > 0: 16 time.sleep(3) #模拟购票等耗时 17 dic["count"] -= 1 18 json.dump(dic,open("db.txt","w",encoding="utf-8")) 19 print("<%s>购票成功" % name) 20 21 22 def task(name,mutex): 23 search(name) 24 with mutex: #相当于mutex.acquire,执行完自代码块自动执行mutex.release() 25 get(name) 26 27 28 if __name__ == "__main__": 29 mutex = Lock() 30 for i in range(10): 31 name = "路人%s" % i 32 p = Process(target=task,args=(name,mutex)) 33 p.start() 34 35 运行结果如下: 36 <路人4> 查看剩余票数[1] 37 <路人7> 查看剩余票数[1] 38 <路人0> 查看剩余票数[1] 39 <路人6> 查看剩余票数[1] 40 <路人2> 查看剩余票数[1] 41 <路人9> 查看剩余票数[1] 42 <路人1> 查看剩余票数[1] 43 <路人3> 查看剩余票数[1] 44 <路人8> 查看剩余票数[1] 45 <路人5> 查看剩余票数[1] 46 <路人4>购票成功
加锁处理,购票行为由并发变成串行,保证了数据安全。
四、互斥锁和join
使用join可以将并发变成串行,互斥锁的原理也是将并发变成串行,那我们直接使用join就可以了啊,为何还要互斥锁?
使用join将并发改成穿行,确实能保证数据安全,但问题是连查票操作也变成只能一个一个人去查了,很明显大家查票时应该是并发地去查询而无需考虑数据准确与否,
此时join与互斥锁的区别就显而易见了,join是将一个任务整体串行,而互斥锁的好处则是可以将一个任务中的某一段代码串行,比如只让task函数中的get任务串行
五、总结
加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行地修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
- 效率低(共享数据基于文件,而文件是硬盘上的数据)
- 需要自己加锁处理
因此我们最好找寻一种解决方案能够兼顾:
- 效率高(多个进程共享一块内存的数据)
- 帮我们处理好锁问题。
这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。
队列和管道都是将数据存放于内存中,而队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,因而队列才是进程间通信的最佳选择。