> 这是并发模型:线程与锁 的第二篇,第一篇地址为: 《并发模型:线程与锁(1)》https://mp.weixin.qq.com/s/6Xxhw31yJNUCh-79Sg8ckQ
超越内置锁
可重入锁
Lock() 虽然方便,但限制很多:
一个线程因为等待内置锁而进入阻塞之后,就无法中断该线程
Lock() 不知道当前拥有锁的线程是否是当前线程,如果当前线程获取了锁,再次获取也会阻塞。
重入锁是(threading.RLock)一个可以被同一个线程多次获取的同步基元组件。在内部,它在基元锁的锁定/非锁定状态上附加了 "所属线程" 和 "递归等级" 的概念。在锁定状态下,某些线程拥有锁 ; 在非锁定状态下, 没有线程拥有它。
若要锁定锁,线程调用其 acquire() 方法;一旦线程拥有了锁,方法将返回。若要解锁,线程调用 release() 方法。 acquire()/release() 对可以嵌套;只有最终 release() (最外面一对的 release() ) 将锁解开,才能让其他线程继续处理 acquire() 阻塞。
threading.RLock 提供了显式的 acquire() 和 release() 方法 一个好的实践是:
lock = threading.RLock()
Lock 和 RLock 的使用区别如下:
#rlock_tut.py
import threading
num = 0
lock = Threading.Lock()
lock.acquire()
num += 1
lock.acquire() # 这里会被阻塞
num += 2
lock.release()
# With RLock, that problem doesn’t happen.
lock = Threading.RLock()
lock.acquire()
num += 3
lock.acquire() # 不会被阻塞.
num += 4
lock.release()
lock.release() # 两个锁都需要调用 release() 来释放.
超时
使用内置锁时,阻塞的线程无法被中断,程序不能从死锁恢复,可以给锁设置超时时间来解决这个问题。
timeout 参数需要 python3.2+
import time
from threading import Thread, Lock
lock1 = RLock()
lock2 = RLock()
# 这个程序会一直死锁下去,如果想突破这个限制,可以在获取锁的时候加上超时时间
# > python threading 没有实现 销毁(destroy),停止(stop),暂停(suspend),继续(resume),中断(interrupt)等
class T1(Thread):
def run(self):
print("start run T1")
lock1.acquire()
# lock1.acquire(timeout=2) # 设置超时时间可避免死锁
time.sleep(1)
lock2.acquire()
# lock2.acquire(timeout=2) # 设置超时时间可避免死锁
lock1.release()
lock2.release()
class T2(Thread):
def run(self):
print("start run T2")
lock2.acquire()
# lock2.acquire(timeout=2) # 设置超时时间可避免死锁
time.sleep(1)
lock1.acquire()
# lock1.acquire(timeout=2) # 设置超时时间可避免死锁
lock2.release()
lock1.release()
def test():
t1, t2 = T1(), T2()
t1.start()
t2.start()
t1.join()
t2.join()
if __name__ == "__main__":
test()
交替锁
如果我们要在链表中插入一个节点。一种做法是用锁保护整个链表,但链表加锁时其它使用者无法访问。交替锁可以只所追杀链表的一部分,允许不涉及被锁部分的其它线程自由访问。
from random import randint
from threading import Thread, Lock
class Node(object):
def __init__(self, value, prev=None, next=None):
self.value = value
self.prev = prev
self.next = next
self.lock = Lock()
class SortedList(Thread):
def __init__(self, head):
Thread.__init__(self)
self.head = head
def insert(self, value):
head = self.head
node = Node(value)
print("insert: %d" % value)
while True:
if head.value <= value:
if head.next != None:
head = head.next
else:
head.lock.acquire()
head.next = node
node.prev = head
head.lock.release()
break
else:
prev = head.prev
prev.lock.acquire()
head.lock.acquire()
if prev != None:
prev.next = node
else:
self.head = node
node.prev = prev
prev.lock.release()
node.next = head
head.prev = node
head.lock.release()
break
def run(self):
for i in range(5):
self.insert(randint(10, 20))
def test():
head = Node(10)
t1 = SortedList(head)
t2 = SortedList(head)
t1.start()
t2.start()
t1.join()
t2.join()
while head:
print(head.value)
head = head.next
if __name__ == "__main__":
test()
这种方案不仅可以让多个线程并发的进行链表插入操作,还能让其他的链表操作安全的并发。
条件变量
并发编程经常需要等待某个事件发生。比如从队列删除元素前需要等待队列非空、向缓存添加数据前需要等待缓存有足够的空间。条件变量就是为这种情况设计的。
条件变量总是与某种类型的锁对象相关联,锁对象可以通过传入获得,或者在缺省的情况下自动创建。当多个条件变量需要共享同一个锁时,传入一个锁很有用。锁是条件对象的一部分,不必单独地跟踪它。
条件变量服从上下文管理协议:使用 with 语句会在它包围的代码块内获取关联的锁。 acquire() 和 release() 方法也能调用关联锁的相关方法。
其它方法必须在持有关联的锁的情况下调用。 wait() 方法释放锁,然后阻塞直到其它线程调用 notify() 方法或 notify_all() 方法唤醒它。一旦被唤醒, wait() 方法重新获取锁并返回。它也可以指定超时时间。
#condition_tut.py
import random, time
from threading import Condition, Thread
"""
'condition' variable will be used to represent the availability of a produced
item.
"""
condition = Condition()
box = []
def producer(box, nitems):
for i in range(nitems):
time.sleep(random.randrange(2, 5)) # Sleeps for some time.
condition.acquire()
num = random.randint(1, 10)
box.append(num) # Puts an item into box for consumption.
condition.notify() # Notifies the consumer about the availability.
print("Produced:", num)
condition.release()
def consumer(box, nitems):
for i in range(nitems):
condition.acquire()
condition.wait() # Blocks until an item is available for consumption.
print("%s: Acquired: %s" % (time.ctime(), box.pop()))
condition.release()
threads = []
"""
'nloops' is the number of times an item will be produced and
consumed.
"""
nloops = random.randrange(3, 6)
for func in [producer, consumer]:
threads.append(Thread(target=func, args=(box, nloops)))
threads[-1].start() # Starts the thread.
for thread in threads:
"""Waits for the threads to complete before moving on
with the main script.
"""
thread.join()
print("All done.")
原子变量
与锁相比使用原子变量的优点:
不会忘记在正确的时候获取锁
由于没有锁的参与,对原子变量的操作不会引发死锁。
原子变量时无锁(lock-free)非阻塞(non-blocking)算法的基础,这种算法可以不用锁和阻塞来达到同步的目的。
python 不支持原子变量
总结
优点
线程与锁模型最大的优点是适用面广,更接近于“本质”--近似于对硬件工作方式的形式化--正确使用时效率高。 此外,线程与锁模型也可轻松的集成到大多数编程语言。
缺点
线程与锁模型没有为并行提供直接的支持
线程与锁模型只支持共享内存模型,如果要支持分布式内存模型,就需要寻求其他技术的帮助。
用线程与锁模型编写的代码难以测试(比如死锁问题可能很久才会出现),出了问题后很难找到问题在哪,并且bug难以复现
代码难以维护(要保证所有对象的同步都是正确的、必须按 顺序来获取多把锁、持有锁时不调用外星方法。还要保证维护代码的开发者都遵守这个规则
参考链接
Let’s Synchronize Threads in Python
哲学家进餐问题
References
[1] 哲学家进餐问题: https://zh.wikipedia.org/wiki/%E5%93%B2%E5%AD%A6%E5%AE%B6%E5%B0%B1%E9%A4%90%E9%97%AE%E9%A2%98
[2] Let’s Synchronize Threads in Python: https://hackernoon.com/synchronization-primitives-in-python-564f89fee732?gi=ce162d119247
[3] 并发模型:线程与锁(1)https://mp.weixin.qq.com/s/6Xxhw31yJNUCh-79Sg8ckQ
最后,感谢女朋友支持和包容,比❤️
也可以在公号输入以下关键字获取历史文章:公号&小程序
| 设计模式
| 并发&协程