主要参考文章:https://www.cnblogs.com/ArsenalfanInECNU/p/10022740.html
1.线程安全与锁
上一篇文章:https://blog.csdn.net/IanWatson/article/details/104727640说道,当对全局资源存在写操作时,如果不能保证写入过程的原子性,会出现脏读脏写的情况,即线程不安全。Python的GIL只能保证原子操作的线程安全,而类似加法这种操作不是一种原子操作,因此在多线程编程时我们需要通过加锁来保证线程安全。
为了保证函数的原子性操作,线程中引入了锁的概念。
最简单的锁是互斥锁(同步锁),互斥锁是用来解决io密集型场景产生的计算错误,即目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据。互斥锁可以解决多线程中修改count大小这一问题。
2. Threading.Lock实现互斥锁(mutex)
上一篇文章举了一个线程不安全的例子:
import threading
count = 0
def run_thread():
global count
for i in range(10000):
count += 1
def run_add():
t_list = []
for i in range(10):
t = threading.Thread(target=run_thread, args=())
t_list.append(t)
t.start()
for t in t_list:
t.join()
run_add()
print(count)
每个线程使用的字节码数大于GIL锁的上限,会导致count的加和失败。互斥锁可以解决这个问题。互斥锁可以保证在使用此线程的时候不被打断,直至线程中所有的操作都完成。互斥锁的写法:
import threading
count = 0
lock = threading.Lock()
def run_thread():
global count
if lock.acquire(1):
# acquire()是获取锁,acquire(1)返回获取锁的结果,成功获取到互斥锁为True,如果没有获取到互斥锁则返回False
for i in range(10000):
count += 1
lock.release() #用完锁之后需要释放
def run_add():
t_list = []
for i in range(10):
t = threading.Thread(target=run_thread, args=())
t_list.append(t)
t.start()
for t in t_list:
t.join()
run_add()
print(count)
互斥锁也可以使用with的写法,和打开文件相似,with使用锁之后会自动释放,省去了release的操作:
import threading
count = 0
lock = threading.Lock()
def run_thread():
global count
with lock:
for i in range(10000):
count += 1
def run_add():
t_list = []
for i in range(10):
t = threading.Thread(target=run_thread, args=())
t_list.append(t)
t.start()
for t in t_list:
t.join()
run_add()
print(count)
3. 死锁的产生及处理
3.1 迭代死锁与递归锁(Rlock)
递归锁介绍:https://www.cnblogs.com/bigberg/p/7910024.html#_label0
标准的锁对象(threading.Lock)并不关心当前是哪个线程占有了该锁;如果该锁已经被占有了,那么任何其它尝试获取该锁的线程都会被阻塞,包括已经占有该锁的线程也会被阻塞。于是当一个线程“迭代”请求同一个资源,直接就会造成死锁。
死锁是指一个资源被多次调用,而多次调用方都未能释放该资源就会造成一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
下面来那个种情况会导致思索的发生:
3.1.1 一个线程内部多次加锁却没有释放
import threading
lock = threading.Lock()
n1 = 0
n2 = 0
def run():
global n1, n2
lock.acquire()
n1 = n1 + 1
print('set n1 to %s' % str(n1))
lock.acquire()
n2 = n2 + 1
print('set n2 to %s' % str(n2))
lock.release()
lock.release()
def run_thread():
th_list = []
for i in range(100):
t = threading.Thread(target=run, args=())
t.start()
th_list.append(t)
for t_each in th_list:
t_each.join()
run_thread()
print(n1, n2)
输出为:
python dead_lock.py
set n1 to 1
然后一直等待
3.1.2 多个程序间相互调用引起死锁
import threading
num_list = [0, 0]
lock = threading.Lock()
def run_1():
global num_list
with lock:
num_list[0] = num_list[0] + 1
print('set num_list[0] to %s' % str(num_list[0]))
def run_2():
global num_list
with lock:
num_list[1] = num_list[1] + 1
print('set num_list[1] to %s' % str(num_list[1]))
def run_3():
with lock:
run_1()
run_2()
print(num_list)
def run_add():
t_list = []
for i in range(10):
t = threading.Thread(target=run_3, args=())
t_list.append(t)
t.start()
for t in t_list:
t.join()
run_add()
while threading.active_count() != 1:
print(threading.active_count())
print(num_list)
示例中,我们有一个共享资源num_list,有两个函数(run_1和run_2)分别取这个共享资源第一部分和第二部分的数字(num_list[0]和num[1])。两个访问函数都使用了锁来确保在获取数据时没有其它线程修改对应的共享数据。
现在,如果我们思考如何添加第三个函数来获取两个部分的数据。一个简单的方法是依次调用这两个函数,然后返回结合的结果。
这里的问题是,如有某个线程在两个函数调用之间修改了共享资源,那么我们最终会得到不一致的数据。
最明显的解决方法是在这个函数中也使用lock(run_3)。然而,这是不可行的。里面的两个访问函数将会阻塞,因为外层语句已经占有了该锁。
结果是没有任何输出,死锁。
3.1.3 递归锁
解决方法是直接将锁改为递归锁RLock()。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。:
import threading
num_list = [0, 0]
lock = threading.RLock()
def run_1():
global num_list
with lock:
num_list[0] = num_list[0] + 1
print('set num_list[0] to %s' % str(num_list[0]))
def run_2():
global num_list
with lock:
num_list[1] = num_list[1] + 1
print('set num_list[1] to %s' % str(num_list[1]))
def run_3():
with lock:
run_1()
run_2()
print(num_list)
def run_add():
t_list = []
for i in range(3):
t = threading.Thread(target=run_3, args=())
t_list.append(t)
t.start()
for t in t_list:
t.join()
run_add()
while threading.active_count() != 1:
print(threading.active_count())
print(num_list)
输出为:
python mul_thread.py
set num_list[0] to 1
set num_list[1] to 1
[1, 1]
set num_list[0] to 2
set num_list[1] to 2
[2, 2]
set num_list[0] to 3
set num_list[1] to 3
[3, 3]
[3, 3]
3.1 互相等待死锁与锁的升序使用
上下文管理器模块contextlib:https://www.cnblogs.com/pyspark/articles/8819803.html
threading的local类:https://www.cnblogs.com/i-honey/p/8051668.html
案例解析:https://www.jb51.net/article/86617.htm
死锁的另外一个原因是两个进程想要获得的锁已经被对方进程获得,只能互相等待又无法释放已经获得的锁,而导致死锁。假设银行系统中,转账函数使用两个锁实现,即先扣掉一方的金额,再向另外一方添加金额。用户a试图转账100块给用户b,与此同时用户b试图转账500块给用户a,则可能产生死锁:
import threading
class Account():
def __init__(self, name, balance, lock):
self.name = name
self.balance = balance
self.lock = lock
def withdraw(self, amount):
self.balance = self.balance - amount
def deposit(self, amount):
self.balance = self.balance + amount
def transfer(from_acc, to_acc, amount):
print("before transfer:%s:%s, %s:%s" % (from_acc.name, from_acc.balance, to_acc.name, to_acc.balance))
with from_acc.lock:
from_acc.withdraw(amount)
print("get lock from %s" % from_acc.name)
with to_acc.lock:
to_acc.deposit(amount)
print("get lock from %s" % to_acc.name)
print("transfer over")
print("after transfer:%s:%s, %s:%s" % (from_acc.name, from_acc.balance, to_acc.name, to_acc.balance))
if __name__ == '__main__':
th_list = []
acc_a = Account('a', 100, threading.Lock())
acc_b = Account('b', 100, threading.Lock())
a_to_b_th = threading.Thread(target=transfer, args=(acc_a, acc_b, 10))
b_to_a_th = threading.Thread(target=transfer, args=(acc_b, acc_a, 20))
th_list.append(a_to_b_th)
th_list.append(b_to_a_th)
for t in th_list:
t.start()
for t in th_list:
t.join()
输出为:
before transfer:a:100, b:100
before transfer:b:100, a:100
get lock from a
get lock from b
2个线程互相等待对方的锁,互相占用着资源不释放,造成死锁。
即我们的问题是:
多线程程序中,线程需要一次获取多个锁,此时如何避免死锁问题。
解决方案:
在多线程程序中,死锁问题很大一部分是由于线程同时获取多个锁造成的。举个例子:一个线程获取了第一个锁,然后在获取第二个锁的 时候发生阻塞,那么这个线程就可能阻塞其他线程的执行,从而导致整个程序假死。 其实解决这个问题,核心思想也特别简单:目前我们遇到的问题是两个线程想获取到的锁,都被对方线程拿到了,那么我们只需要保证在这两个线程中,获取锁的顺序保持一致就可以了。举个例子,我们有线程thread_a, thread_b, 锁lock_1, lock_2。只要我们规定好了锁的使用顺序,比如先用lock_1,再用lock_2,当线程thread_a获得lock_1时,其他线程如thread_b就无法获得lock_1这个锁,也就无法进行下一步操作(获得lock_2这个锁),也就不会导致互相等待导致的死锁。简言之,解决死锁问题的一种方案是为程序中的每一个锁分配一个唯一的id,然后只允许按照升序规则来使用多个锁,这个规则使用上下文管理器 是非常容易实现的,示例如下:
# /usr/bin/python3
# -*- coding: utf-8 -*-
import threading
import time
from contextlib import contextmanager
thread_local = threading.local()
@contextmanager
def acquire(*locks):
# sort locks by object identifier
locks = sorted(locks, key=lambda x: id(x))
acquired = getattr(thread_local, 'acquired', [])
# Acquire all the locks
acquired.extend(locks)
thread_local.acquired = acquired
try:
for lock in locks:
lock.acquire()
yield
finally:
for lock in reversed(locks):
lock.release()
del acquired[-len(locks):]
class Account(object):
def __init__(self, name, balance, lock):
self.name = name
self.balance = balance
self.lock = lock
def withdraw(self, amount):
self.balance -= amount
def deposit(self, amount):
self.balance += amount
def transfer(from_account, to_account, amount):
print("%s transfer..." % amount)
with acquire(from_account.lock, to_account.lock):
from_account.withdraw(amount)
time.sleep(1)
to_account.deposit(amount)
print("%s transfer... %s:%s ,%s: %s" % (
amount, from_account.name, from_account.balance, to_account.name, to_account.balance))
print("transfer finish")
if __name__ == "__main__":
a = Account('a', 1000, threading.Lock())
b = Account('b', 1000, threading.Lock())
thread_list = []
thread_list.append(threading.Thread(target=transfer, args=(a, b, 100)))
thread_list.append(threading.Thread(target=transfer, args=(b, a, 500)))
for i in thread_list:
i.start()
for j in thread_list:
j.join()
输出:
100 transfer...
500 transfer...
100 transfer... a:900 ,b: 1100
transfer finish
500 transfer... b:600 ,a: 1400
transfer finish