Concepts
想象一下 5 位哲学家坐在一张圆桌旁,桌子中间放着一碗面条。 一根筷子夹在每个哲学家之间。 哲学家只有拿着两根筷子才能吃饭。 吃完后,他们放下筷子,让另一个哲学家吃。
需要想出一个办法让所有的哲学家都有机会吃饭。 这是对 Edsger Dijkstra 在 1965 年向他的计算机科学专业学生提出的经典哲学家就餐问题的描述。这是一个类比,用于演示多线程系统中的饥饿和死锁。 哲学家就像任务(或线程)试图使用共享资源(一碗面条)执行某些工作。 筷子类似于在访问共享资源之前需要的锁(信号量或互斥锁)。
如果有两位哲学家的优先级较高,并且不放下筷子,那么其他的哲学家(线程)就有吃不到东西的风险。这叫做“饥饿”。
有几种方法可以对抗饥饿。第一种就是确保高优先级的任务总是为处理器腾出一些时间(例如,等待mutex/semaphore或 通过延迟函数让自己睡眠)。
第二个方法可以内置到调度器 或者 用一个更高优先级task去监控其他task 睡眠了多久。如果一个低优先级的任务已经睡眠(blocked)了一段时间,那么它的优先级就会提升到让它有机会run。一旦它run了一段时间,它的优先级就会降到原来级别。这叫做 “aging”(老化)。
随着老化,也可能会出现另一个问题。 假设我们针对每个哲学家的算法如下:
- 拿起左筷子
- 拿起右边的筷子
- 吃一会
- 放下左筷子
- 放下右手筷子
如果每个哲学家都捡起他们左手边筷子,然后立即被另一个哲学家捡起左手边筷子打断,我们现在就到了 所有哲学家都拿着左手边筷子这一步。
但是,没人能拿起右手边筷子,因为所有哲学家都在等右手边筷子被释放。这个现象叫“死锁”。系统停止了,因为所有线程都在循环等待锁被释放。
一种解决方法是给每根筷子一个优先级。我们可以修改算法 为每个哲学家都必须先捡起最小的数字(最大的也可以,不重要)。
这会防止死锁发生,因为最后一位哲学家必须等待编号最小的筷子返回,然后才能拿起另一根筷子。
另一种解决方法是引入“服务员”或“仲裁员 arbitrator”。 这个 arbitrator 本质上用一个全局互斥锁锁定了所有筷子。
任何时候一个哲学家希望捡起任何筷子的时候,他们必须向这个arbitrator请求允许。这个mutex(arbitrator)减少值,让这个哲学家拿起两支筷子吃饭。只要第一位哲学家拥有互斥锁,任何其他想要拿起筷子的哲学家都会被拒绝。
这可以作为防止死锁的解决方案,但它效率很低,即不能让多个哲学家同时吃饭(并行性)。
Recommended Reading
introduction-to-rtos-solution-to-part-10-deadlock-and-starvation:https://www.digikey.com/en/maker/projects/introduction-to-rtos-solution-to-part-10-deadlock-and-starvation/872c6a057901432e84594d79fcb2cc5d
The Dining Philosophers challenge was ported to Arduino from this University of Virginia course’s lab: http://www.cs.virginia.edu/luther/COA2/S2019/pa05-dp.html
You can try the Dining Philosophers challenge without an Arduino here (using C++): https://leetcode.com/problems/the-dining-philosophers/
Another good article discussing deadlock on FreeRTOS: https://microcontrollerslab.com/freertos-recursive-mutex-avoid-deadlocks-examples-arduino/