写点什么

Python 并发编程之死锁

作者:宇宙之一粟
  • 2022 年 8 月 24 日
    中国香港
  • 本文字数:2054 字

    阅读完需:约 7 分钟

Python 并发编程之死锁

前言

在并发编程中,死锁指的是一种特定的情况,即无法取得进展,程序被锁定在其当前状态。在大多数情况下,这种现象是由于不同的锁对象(用于线程同步)之间缺乏协调,或者处理不当造成的。在这一节中,我们将讨论一个思想实验,通常被称为餐饮哲学家问题,以说明死锁的概念及其原因;从这里开始,你将学习如何在 Python 并发程序中模拟这个问题。

哲学家就餐问题

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


在 1971 年,著名的计算机科学家艾兹格·迪科斯彻提出了一个同步问题,即假设有五台计算机都试图访问五份共享的磁带驱动器。稍后,这个问题被托尼·霍尔重新表述为哲学家就餐问题。这个问题可以用来解释死锁和资源耗尽



假如有 5 个哲学家,围坐在一起,每个人面前有一碗饭和一只筷子。在这里每个哲学家可以看做是一个独立的线程,而每只筷子可以看做是一个锁。


他们每个人都需要两个叉子来吃饭。如果他们同时拿起他们左边的叉子,那么它将一直等待右边的叉子被释放。每个哲学家可以处在静坐、 思考、吃饭三种状态中的一个。需要注意的是,每个哲学家吃饭是需要两只筷子的,这样问题就来了:如果每个哲学家都拿起自己左边的筷子, 那么他们五个都只能拿着一只筷子坐在那儿,直到饿死。此时他们就进入了死锁状态。


下面是一个简单的使用死锁避免机制解决“哲学家就餐问题”的实现:


import threading
# The philosopher threaddef philosopher(left, right): while True: with acquire(left,right): print(threading.currentThread(), 'eating')
# The chopsticks (represented by locks)NSTICKS = 5chopsticks = [threading.Lock() for n in range(NSTICKS)]
# Create all of the philosophersfor n in range(NSTICKS): t = threading.Thread(target=philosopher, args=(chopsticks[n],chopsticks[(n+1) % NSTICKS])) t.start()
复制代码


最后,要特别注意到,为了避免死锁,所有的加锁操作必须使用 acquire() 函数。如果代码中的某部分绕过 acquire 函数直接申请锁,那么整个死锁避免机制就不起作用了。

死锁的常见例子

造成线程死锁的常见例子包括:


  1. 一个在自己身上等待的线程(例如,试图两次获得同一个互斥锁)

  2. 互相等待的线程(例如,A 等待 B,B 等待 A)

  3. 未能释放资源的线程(例如,互斥锁、信号量、屏障、条件、事件等)

  4. 线程以不同的顺序获取互斥锁(例如,未能执行锁排序)

模拟死锁:线程等待本身

导致死锁的一个常见原因是线程在自己身上等待。


我们并不打算让这种死锁发生,例如,我们不会故意写代码,导致线程自己等待。相反,由于一系列的函数调用和变量的传递,这种情况会意外地发生。


一个线程可能会因为很多原因而在自己身上等待,比如:


  • 等待获得它已经获得的互斥锁

  • 等待自己被通知一个条件

  • 等待一个事件被自己设置

  • 等待一个信号被自己释放


开发一个 task() 函数,直接尝试两次获取同一个 mutex 锁。也就是说,该任务将获取锁,然后再次尝试获取锁。


# task to be executed in a new thread
def task(lock):
print('Thread acquiring lock...')
with lock:
print('Thread acquiring lock again...')
with lock:
# will never get here
pass
复制代码


这将导致死锁,因为线程已经持有该锁,并将永远等待自己释放该锁,以便它能再次获得该锁, task() 试图两次获取同一个锁并触发死锁。


在主线程中,可以创建锁:


# create the mutex locklock = Lock()
复制代码


然后我们将创建并配置一个新的线程,在一个新的线程中执行我们的 task() 函数,然后启动这个线程并等待它终止,而它永远不会终止。


# create and configure the new threadthread = Thread(target=task, args=(lock,))# start the new threadthread.start()# wait for threads to exit...thread.join()
复制代码


完整代码如下:


from threading import Threadfrom threading import Lock
# task to be executed in a new threaddef task(lock): print('Thread acquiring lock...') with lock: print('Thread acquiring lock again...') with lock: # will never get here pass
# create the mutex locklock = Lock()# create and configure the new threadthread = Thread(target=task, args=(lock,))# start the new threadthread.start()# wait for threads to exit...thread.join()
复制代码


运行结果如下:



首先创建锁,然后新的线程被混淆并启动,主线程阻塞,直到新线程终止,但它从未这样做。


新线程运行并首先获得了锁。然后它试图再次获得相同的互斥锁并阻塞。


它将永远阻塞,等待锁被释放。该锁不能被释放,因为该线程已经持有该锁。因此,该线程已经陷入死锁。


该程序必须被强制终止,例如,通过 Control-C 杀死终端。


参考链接:


发布于: 12 小时前阅读数: 46
用户头像

宇宙古今无有穷期,一生不过须臾,当思奋争 2020.05.07 加入

🏆InfoQ写作平台-第二季签约作者 🏆 混迹于江湖,江湖却没有我的影子 热爱技术,专注于后端全栈,轻易不换岗 拒绝内卷,工作于软件工程师,弹性不加班 热衷分享,执着于阅读写作,佛系不水文

评论

发布
暂无评论
Python 并发编程之死锁_Python_宇宙之一粟_InfoQ写作社区