并发编程互斥锁

Posted chuanxiaopang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程互斥锁相关的知识,希望对你有一定的参考价值。

一、什么是互斥锁

由于并发状态下,操作系统对多个进程进行调度,而多个进程可能都有操作硬件的需求,这时就会产生多个进程对资源的共享,而共享意味着竞争,竞争会产生许多问题。这样就需要一种机制或者手段去解决竞争,使竞争变得有序化,从而使共享资源按照预定的结果去获取。这种手段就是加互斥锁。

使用互斥锁会由并发改为串行,牺牲了运行效率,但保证了数据安全。

二、代码示例

在并发状态下,如果没有互斥锁,多个进程竞争同一硬件,会出现错乱。这就好比多个人合租同一套房,会出现争抢卫生间的现象。一个人上卫生间后上一把锁,其他人都要等着,等到完成这个任务后释放锁。如果没有互斥锁,会产生下列现象:

技术分享图片
 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()
View Code

运行结果如下:

技术分享图片

这种现象是不应该发生的。可以引入Lock加上互斥锁。

技术分享图片
 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()
View Code

运行结果如下:

技术分享图片

可以发现,由并发改为串行,虽然降低了效率,但是保证了数据安全不错乱。

三、模拟抢票练习

购买火车票的场景比较典型。多个用户可以同时查到的实时余票信息,但购买火车票时是有先后顺序的。下面用文件db.txt当数据库,用多个进程模拟多个人执行抢票任务。

db.txt

技术分享图片
1 {"count":1}
View Code

模拟当前仅剩一张余票,多个用户同时看到,但只能有一个人抢到

模拟抢票.py

技术分享图片
 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>购票成功
View Code

加锁处理,购票行为由并发变成串行,保证了数据安全。

四、互斥锁和join

使用join可以将并发变成串行,互斥锁的原理也是将并发变成串行,那我们直接使用join就可以了啊,为何还要互斥锁?

使用join将并发改成穿行,确实能保证数据安全,但问题是连查票操作也变成只能一个一个人去查了,很明显大家查票时应该是并发地去查询而无需考虑数据准确与否,

此时join与互斥锁的区别就显而易见了,join是将一个任务整体串行,而互斥锁的好处则是可以将一个任务中的某一段代码串行,比如只让task函数中的get任务串行

五、总结

加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行地修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。

虽然可以用文件共享数据实现进程间通信,但问题是:

  1. 效率低(共享数据基于文件,而文件是硬盘上的数据)
  2. 需要自己加锁处理

因此我们最好找寻一种解决方案能够兼顾:

  1. 效率高(多个进程共享一块内存的数据)
  2. 帮我们处理好锁问题。

这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。

队列和管道都是将数据存放于内存中,而队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,因而队列才是进程间通信的最佳选择。

 

以上是关于并发编程互斥锁的主要内容,如果未能解决你的问题,请参考以下文章

Go并发编程之传统同步—互斥锁

9 并发编程-(线程)-守护线程&互斥锁

Python并发编程03/僵尸孤儿进程,互斥锁,进程之间的通信

并发编程(互斥锁)

并发编程--线程开启线程守护线程线程互斥锁

Go并发编程之美-互斥锁