如何防止进餐哲学家C ++陷入僵局

时间:2019-04-12 14:25:51

标签: c++ multithreading deadlock

我正试图解决餐饮哲学家问题的僵局。我已经有一个由老师提供的代码框架。

我尝试使用try_lock()修复问题

chopstick[(i+1)%5].try_lock(); 

但是这并不能解决我的问题,当我运行它时,确实收到了以下错误消息: 错误“解锁未拥有的互斥锁”。

我还尝试通过做以下更改来解决该问题,就像在youtube视频中看到的那样

chopstick[i].lock();

chopstick[min(i,(i+1)%5)].lock();

还有

chopstick[(i+1)%5].lock();

chopstick[max(i,(i+1)%5)].lock();

这是我所提供的骨骼。

#include <windows.h>
#include <stdio.h>
#include <iostream>
#include <vector>
#include <algorithm>
#include <thread>
#include <mutex>
#include <time.h>

using namespace std;

thread task[5];
mutex chopstick[5];
int stop = false;

void go(int i) {
    while (!stop) {

        chopstick[i].lock();

        cout << i << ": takes: " << i << endl;
        chrono::milliseconds dur(20);
        this_thread::sleep_for(dur); //Leads to deadlock immediately
        chopstick[(i + 1) % 5].lock();

        cout << i << ": eating" << endl;

        chrono::milliseconds dur2(rand() % 200 + 100);
        this_thread::sleep_for(dur2);

        chopstick[(i + 1) % 5].unlock();
        chopstick[i].unlock();
    }
}
int main() {
    srand(time(NULL));

    for (int i = 0; i < 5; ++i) {
        task[i] = (thread(go, i));
    }
    for (int i = 0; i < 5; i++) {
        task[i].join();
    }

}

我从理论上理解餐饮哲学家,但我无法解决这个问题。我不太了解自己在做什么错。有人可以解释我在做什么错并帮助我解决吗?

3 个答案:

答案 0 :(得分:3)

(带有锁)并行编程的核心规则之一是,您应始终以相同顺序获取锁。

在您的代码中,每个任务首先获得其锁定,然后获得下一个锁定。一种解决方案是始终从偶数索引中获取锁,然后才从奇数索引中获取锁。这样,您获取锁的顺序将保持一致。

另一种众所周知的策略是“退避”,在该退避中,您使用// List.BinarySearch returns: // The zero-based index of item in the sorted System.Collections.Generic.List`1, // if item is found; otherwise, a negative number that is the bitwise complement // of the index of the next element that is larger than item or, if there is no // larger element, the bitwise complement of System.Collections.Generic.List`1.Count. int pos = lists.BinarySearch("d"); int resultPos = pos >= 0 ? pos : ~pos - 1; Console.WriteLine("Result: " + resultPos); 和第一个lock()来获取第一把锁,如果无法获得,则释放所有获得的锁。锁定并重新开始该序列。就性能而言,此策略并不理想,但可以保证最终可以使用。

答案 1 :(得分:0)

解决死锁的最简单方法是使用为此目的而发明的std::lock(l1, l2)

更改:

    chopstick[i].lock();

    cout << i << ": takes: " << i << endl;
    chrono::milliseconds dur(20);
    this_thread::sleep_for(dur); //Leads to deadlock immediately
    chopstick[(i + 1) % 5].lock();

收件人:

    std::lock(chopstick[i], chopstick[(i + 1) % 5]);

    cout << i << ": takes: " << i << endl;

这是一个简单明了的解决方案,它忽略了异常安全性,这对于减少您的第一个避免死锁的课程是很好的。

为使其具有异常安全性,互斥体需要包装在RAII设备中:std::unique_lock

    unique_lock<mutex> left{chopstick[i], defer_lock};
    unique_lock<mutex> right{chopstick[(i + 1) % 5], defer_lock};
    lock(left, right);

    cout << i << ": takes: " << i << endl;

然后您还应该删除显式的unlock语句,因为leftright的析构函数将解决此问题。

现在,如果在go中引发异常,则leftright的析构函数将在异常传播时解锁互斥体。

要详细了解std::lock内为避免死锁所发生的情况,请参阅:http://howardhinnant.github.io/dining_philosophers.html

性能测试

这是一个快速简便的测试,可以将std::lock的使用与更传统的“订购互斥锁”建议进行比较。

#ifndef USE_STD_LOCK
#   error #define USE_STD_LOCK as 1 to use std::lock and as 0 to use ordering
#endif

#include <atomic>
#include <chrono>
#include <exception>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <thread>

std::thread task[5];
constexpr auto N = sizeof(task)/sizeof(task[0]);
std::mutex chopstick[N];
std::atomic<bool> stop{false};
unsigned long long counts[N] = {};

using namespace std::chrono_literals;

void
go(decltype(N) i)
{
    auto const right = (i + 1) % N;
    decltype(right) const left = i;
    while (!stop)
    {
#if USE_STD_LOCK
        std::lock(chopstick[left], chopstick[right]);
#else
        if (left < right)
        {
            chopstick[left].lock();
            chopstick[right].lock();
        }
        else
        {
            chopstick[right].lock();
            chopstick[left].lock();
        }
#endif
        std::lock_guard<std::mutex> l1{chopstick[left],  std::adopt_lock};
        std::lock_guard<std::mutex> l2{chopstick[right], std::adopt_lock};
        ++counts[i];
        std::this_thread::sleep_for(1ms);
    }
}

void
deadlock_detector(std::chrono::seconds time_out)
{
    std::this_thread::sleep_for(time_out);
    std::cerr << "Deadlock!\n";
    std::terminate();
}

int
main()
{
    for (auto i = 0u; i < N; ++i)
        task[i] = std::thread{go, i};
    std::thread{deadlock_detector, 15s}.detach();
    std::this_thread::sleep_for(10s);
    stop = true;
    for (auto& t : task)
        t.join();
    std::cout << std::right;
    for (auto c : counts)
        std::cout << std::setw(6) << c << '\n';
    auto count = std::accumulate(std::begin(counts), std::end(counts), 0ULL);
    std::cout << "+ ----\n";
    std::cout << std::setw(6) << count << '\n';
}

必须使用已定义的USE_STD_LOCK进行编译:

  1. #define USE_STD_LOCK 0订购互斥锁并按顺序锁定它们。
  2. #define USE_STD_LOCK 1使用std::lock锁定互斥锁。

程序运行10秒钟,每个线程尽可能多地增加一个不同的unsigned long long。但是为了使事情更具戏剧性,每个线程在保持锁的同时也睡眠了1毫秒(如果需要,可以在不睡眠的情况下运行)。

10秒钟之后,main告诉大家该转移已经结束,并计算每个线程的结果以及所有线程的总增量。越高越好。

启用优化后,我得到的数字如下:

USE_STD_LOCK = 1

  3318
  2644
  3254
  3004
  2876
+ ----
 15096

USE_STD_LOCK = 0

    19
    96
  1641
  5885
    50
+ ----
  7691

请注意,使用std::lock不仅会导致很多更高的累积结果,而且每个线程对总数的贡献大致相同。相反,在某些情况下,“排序”趋向于选择一个线程,而使其他线程饿死。

这是在4核Intel Core i5上。我将差异归因于拥有多个内核,以便至少两个线程可以同时运行。如果此程序在单个核心设备上运行,则不会有这种差异(我尚未测试该配置)。

我还用死锁检测器对测试进行了检测。这不会影响我得到的结果。它旨在让人们尝试其他锁定算法,并更快地确定测试是否已锁定。如果此死锁检测器以任何方式困扰您,只需将其从测试运行中删除即可。我不想辩论它的优点。

无论您获得相似的结果还是不同的结果,我都欢迎建设性的反馈。或者,如果您认为此测试偏向于一种或另一种偏见,以及如何使其变得更好。

答案 2 :(得分:0)

存在四(4)个条件,它们是必要的足够的,以产生死锁

死锁条件

  • 资源互斥资源无法共享

所考虑(请求)的资源不得共享。如果允许共享资源,则不会阻止(同级)进程在需要时获取资源。

  • 资源保持并等待或持久部分分配

进程必须拥有已分配的资源,并等待(尝试占用)后续(请求的)资源。当进程必须在请求新资源时释放资源时,则死锁不会发生,因为进程不会阻止(同级)进程在需要时获取资源。

  • 资源不允许抢占过程平等或公平

进程被保留后不能占用资源。否则,较高优先级(等级)的流程将简单地占用(占用)足够的资源以使流程能够完成。许多RTOS使用此方法来防止死锁。

  • 资源循环订单或等待(资源图中存在循环

资源中存在循环排序或循环(链),其中不能按部分顺序(编号为min..max)安排资源。当可以对资源施加部分顺序时,可以遵循该严格顺序来占用(锁定)资源,并且不会发生死锁(请参阅循环定理,该定理指出“资源图中的循环是必要的,因此死锁可能会发生”)。

用餐哲学家问题(通过实验 )被构造为呈现所有四个条件,而挑战则是确定哪个条件)以避免(破坏)。 经典答案之一是更改资源的顺序以打破循环等待条件。每个哲学家都独立决定如何解决僵局。

  • 可共享-每个哲学家都需要两个分叉,并且无法共享。
  • 持之以恒-每个哲学家在拿另一个时都必须保持一把叉子。
  • 没有先发制人-任何哲学家都不会从另一个分支拿走叉子。
  • 循环秩序-有一个循环,所以两个哲学家发生碰撞并陷入僵局。

有几种著名的解决方案:

  • Djikstra的解决方案-为叉子编号(1 .. N),所有哲学家都应按照以下规则进行叉子的使用:先使用编号较低的叉子,然后使用编号较高的叉子,并在发生碰撞时释放任何资源。
  • 仲裁员(监视器)-仲裁员分配叉子,当哲学家想要吃饭时,他们请仲裁员对请求进行序列化,并在可用时分配(废掉)资源(叉子)。

Djikstra是典型的解决方案-为叉子编号。