Python中的并发编程(4):线程同步

要点

本章着重讲解,线程间的数据如何通同步,线程间的竞争机制,死锁和python原生提供的线程同步方法。

哲学家就餐问题

谈及线程竞争,同步问题,就不能不提到哲学家就餐问题。
引自维基百科
img

哲学家就餐问题(英语:Dining philosophers problem)是在计算机科学中的一个经典问题,用来演示在并发计算中多线程同步(Synchronization)时产生的问题。

哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。

哲学家就餐问题的演示
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。

即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。

在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源。一种常用的计算机技术是资源加锁,用来保证在某个时刻,资源只能被一个程序或一段代码访问。当一个程序想要使用的资源已经被另一个程序锁定,它就等待资源解锁。当多个程序涉及到加锁的资源时,在某些情况下就有可能发生死锁。例如,某个程序需要访问两个文件,当两个这样的程序各锁了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生。

竞争条件

我们可以使用术语“竞争条件”来描述上述的问题。竞争条件用于描述进程的输出依赖于不受控制的时间出现顺序或者时机。上面例子的哲学家们,在没有额外干预或者特定规则的时候,拿起和放下餐叉的行为就是不受控制的,随机发生的。所以很有可能出现死锁或者资源耗尽。

线程同步的方法

临界区块

首先需要介绍临界区块,在这个区块里,资源在一个时间只能被一个线程进行访问。所以我们可以使用某种方法,将这一段区域的逻辑(代码)锁起来。注意,在c++的线程同步中,专门有一个临界区,就是使用enterCriticalSection(), leaveCriticalSection()。但是在这里,主要是说明这个概念,说明同步的实质。Python没有这么明显的方法来调用。

Locks

锁,使用方法为:

lock = threading.lock()
lock.aquire()
## logic in critical sections
lock.release()

RLocks

Reentrant-locks,可重入锁。用法和普通的锁相似,但是这个锁可以锁多次(如果当前线程本身就有该资源,这个时候也可以锁)而不会死锁。因为Rlocks里面会包含计数,所以可以锁多次,也需要Release多次。

另外,可以调用
Rlock._is_owned()
如果返回bool,就表明已经获取了当前资源

最简单的使用方式就是使用with, 例如 with myrlocks: 这样仅仅在改区段中生效,在进入和退出区段的时候,计数会自动+1,-1.
另外,通常说来,使用在python中使用线程同步,使用Rlock就够用了。

Condition

线程需要等待某个条件为真的情况下,才会继续执行。例如,要等到某个计算结果出来才会继续运行。适用场景为生产者,消费者模式。

condition = threading.Condition()

condition.acquire() #上锁

condition.wait() # 等待,知道notify发生

condition.notify() # 唤醒被锁住的线程

condition.release() #释放锁资源

Semaphores

信号量。信号量是一个同步对象,用于保持0到最大值的计数值,完成一次等待则计数-1,完成一个release,则计数+1。当计数大于1的时候,为signaled状态,可以执行。默认值为1。其反应的数值为 release- wait(aquire)的次数。

例如可以使用票务系统。

Event

事件。某一个线程可以宣告时间发生。其他监听线程则可以获得这个通知。

# 创建时间
event = threading.Event()
# 判断事件是否发生
event.is_set()
# 设置事件发生
event.set()

Barriers

同步屏障。这也是并行计算的一种同步方法,程序中的一个同步屏障表示线程到此必须等待所有线程到达该点才能进行下一步的操作。

barrier = threading.Barrier(n) # n表示有多少个线程需要在此等待
barrier.wait() # 可以让线程在此等待

Timer

时间对象。只有在经过了一段时间之后,操作才能继续进行。

总结

本文主要介绍了线程同步的概念,以及Python本身提供的线程同步的方式。选择很多。不过我认为一般说来,Rlock就够用了。
最后我们也来简单对比下c++中线程同步的基本方式吧:原子操作,事件,信号量,互斥量,临界区,自旋锁。(我用得最多的好像还是互斥量。。)其实还是很类似的。

参考

猜你喜欢

转载自blog.csdn.net/iuhsihsow/article/details/80266213
今日推荐