多线程实现之互斥锁
问题引入
- 假如存在一个列表:
- A线程对其修改,B进程对其访问
- 假如A,B线程同时去修改和访问则会出现问题
- 一个数据明明要被A线程删掉的,结果B线程抢先一步就把这个数据访问到了
- 那么如何解决这个问题呢?可以使用线程同步
1.1 线程同步:
- 多个线程访问一个对象时,请线程们先排好队,一个一个使用
1.2 互斥锁
-
可以使用互斥锁来实现线程同步。
-
互斥锁的原理就是:先把需要访问的数据上锁,留下一把钥匙。
- 当很多线程来访问时,哪个线程拿到了钥匙,谁就去开锁访问数据。
- 注意钥匙是多个线程抢来的,而不是智能分配的。
# 使用互斥锁的案例 from threading import Thread, Lock from time import sleep class Account: def __init__(self, money, name): self.money = money self.name = name # 模拟提款的操作 class Drawing(Thread): def __init__(self, drawingNum, account): Thread.__init__(self) self.drawingNum = drawingNum self.account = account self.expenseTotal = 0 def run(self): lock1.acquire() # 获得钥匙 if self.account.money < self.drawingNum: print("账户余额不足!") return sleep(1) # 判断完可以取钱,则阻塞。就是为了测试发生冲突问题 self.account.money -= self.drawingNum self.expenseTotal += self.drawingNum lock1.release() # 交出钥匙 print(f"账户:{self.account.name},余额是:{self.account.money}") print(f"账户:{self.account.name},总共取了:{self.expenseTotal}") if __name__ == '__main__': a1 = Account(1000, "python") lock1 = Lock() draw1 = Drawing(600, a1) # 使用组合,定义一个取钱的线程 draw2 = Drawing(450, a1) # 使用组合,定义一个取钱的线程 draw1.start() draw2.start() # 账户:python,余额是:400账户余额不足! # # 账户:python,总共取了:600
1 详解-数据破坏问题
- 每个线程互相独立,相互之间没有任何关系。
- 现在假设这样一个例子:
- 有一个全局的计数num,
- 每个线程获取这个全局的计数,
- 根据num进行一些处理,
- 然后将num加1。
import threading
num = 0 # 全局变量
def work1(number):
global num
for i in range(number):
num += 1
print('此时num的值为',num)
def work2(number):
global num
for i in range(number):
num += 1
print('此时num的值为',num)
if __name__ == '__main__':
t1 = threading.Thread(target=work1,args=(100000000,))
t2 = threading.Thread(target=work1,args=(100000000,))
t1.start()
t2.start()
结果如下:
此时num的值为 198328174
此时num的值为 200000000
- 这个结果显然并不是我们想要的结果,一定程度上对我们想要的数据造成了影响,甚至是破坏
- 问题产生的原因就是没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。
2 引入互斥锁解决数据破坏问题
- 线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
- 互斥锁为资源引入一个状态:锁定/非锁定。
- 某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;
- 直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。
- 互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
#创建锁
lock = threading.Lock()
#锁定
lock.acquire(timeout = 100) # 防止死锁
#释放
lock.release()
- 其中,锁定方法acquire可以有一个超时时间的可选参数timeout。
- 如果设定了timeout,则在超时后通过返回值可以判断是否得到了锁,从而可以进行一些其他的处理。
`import threading
num = 0 # 全局变量
def work1(number):
lock.acquire(timeout=100)
global num
for i in range(number):
num += 1
print('此时num的值为',num)
lock.release()
def work2(number):
lock.acquire(timeout=100)
global num
for i in range(number):
num += 1
print('此时num的值为',num)
lock.release()
lock = threading.Lock()
if __name__ == '__main__':
t1 = threading.Thread(target=work1,args=(100000000,))
t2 = threading.Thread(target=work1,args=(100000000,))
t1.start()
t2.start()
结果:
此时num的值为 100000000
此时num的值为 200000000
- 可以看到,加入互斥锁后,运行结果与预期相符。
2 同步阻塞
-
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
-
每次只有一个线程可以获得锁。
-
如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“同步阻塞”。
-
直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
-
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
-
多线程问题之死锁
- 死锁是指:当一个线程需要多把钥匙时,而另一个线程也需要相同的钥匙,由于错位产生的程序抱死。
- 如:A线程需要1,2两把钥匙才能运行,B线程也需要1,2两把钥匙才能运行,
- 当A线程拿到钥匙1而B线程拿到了钥匙2,
- 导致A线程永远拿不到钥匙2,B线程永远拿不到钥匙1,
- 两个线程相互挂起,程序死机。
- 解决方法就是:避免一个线程拿多把钥匙。
- 如:A线程需要1,2两把钥匙才能运行,B线程也需要1,2两把钥匙才能运行,
from threading import Thread, Lock
from time import sleep
def fun1():
lock1.acquire() # 这个线程拿到第一把钥匙
print('fun1拿到菜刀')
sleep(2)
lock2.acquire() # 这个线程打算拿第二把钥匙,但是第二把钥匙已经被另一个线程拿走了,该线程会在这里一直挂起
print('fun1拿到锅')
lock2.release()
print('fun1释放锅')
lock1.release()
print('fun1释放菜刀')
def fun2():
lock2.acquire() # 这个线程拿到第二把钥匙
print('fun2拿到锅')
lock1.acquire()
print('fun2拿到菜刀')
lock1.release()
print('fun2释放菜刀')
lock2.release()
print('fun2释放锅')
if __name__ == '__main__':
lock1 = Lock()
lock2 = Lock()
t1 = Thread(target=fun1)
t2 = Thread(target=fun2)
t1.start()
t2.start()