c++ 同时锁定与逐个锁定

r3i60tvu  于 2023-08-09  发布在  其他
关注(0)|答案(1)|浏览(91)

在用餐哲学中,同时锁定与逐个锁定有什么区别?即lock(lck1, lck2)lck1.lock() lck2.lock()?我试过他们两个,他们都工作得很好。我读过关于lock(lck1, lck2)的文章,这样我们就可以防止死锁。如果一个被获取了,另一个没有,那么它将释放所获取的一个,并再次尝试获取死锁。但是lock(lck1, lck2)会有什么影响呢?这里会有一些与性能相关的东西。请帮助我是新的多线程。

#include <iostream>
    #include <thread>
    #include <functional>
    #include <chrono>
    #include <vector>
    #include <mutex>
    
    using namespace std;
    
    class DiningPhilosophers {
    private:
        mutex mtx[5];
        
    public:
        DiningPhilosophers() { }
        
        void wantsToEat(int philosopher, function<void()> pickLeftFork, function<void()> pickRightFork, function<void()> eat, function<void()> putLeftFork, function<void()> putRightFork) {
            int left = philosopher;
            int right = (philosopher + 1) % 5;
            
            unique_lock<mutex> lck1(mtx[left], defer_lock); // defer_lock: init lck1 without locking mtx
            unique_lock<mutex> lck2(mtx[right], defer_lock);
            
            if (philosopher % 2 == 0) {
                // lck1.lock(); // do NOT use std::lock(lck1, lck2)
                // lck2.lock();
                lock(lck1, lck2);
                pickLeftFork(); pickRightFork();
            } else {
                lck2.lock();
                lck1.lock();
                pickLeftFork(); pickRightFork();
            }
            eat(); putRightFork(); putLeftFork();
            // lck1 & lck2 are auto released after this line
        }
    };
    
    void testPhilosopher(int philosopherId, DiningPhilosophers& diningPhilosophers) {
        // Lambda functions to simulate the actions of a philosopher
        auto pickLeftFork = [=]() { cout << "Philosopher " << philosopherId << " picked left fork." << endl; };
        auto pickRightFork = [=]() { cout << "Philosopher " << philosopherId << " picked right fork." << endl; };
        auto eat = [=]() {
            cout << "Philosopher " << philosopherId << " is eating." << endl;
            // Simulate eating time
            this_thread::sleep_for(chrono::milliseconds(500));
        };
        auto putLeftFork = [=]() { cout << "Philosopher " << philosopherId << " put down left fork." << endl; };
        auto putRightFork = [=]() { cout << "Philosopher " << philosopherId << " put down right fork." << endl; };
    
        // Call the wantsToEat function for the philosopher
        diningPhilosophers.wantsToEat(philosopherId, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork);
    }
    
    int main() {
        DiningPhilosophers diningPhilosophers;
    
        // Create five threads, each representing a philosopher
        vector<thread> philosopherThreads;
        for (int i = 0; i < 5; ++i) {
            philosopherThreads.emplace_back(testPhilosopher, i, ref(diningPhilosophers));
        }
    
        // Wait for all threads to finish
        for (auto& thread : philosopherThreads) {
            thread.join();
        }
    
        return 0;
    }

字符串

vc6uscn9

vc6uscn91#

我在这个问题下的评论可能是夸张的,老实说我不确定。然而,我可以肯定地说,如果你的代码以相同的相对顺序锁定每个互斥锁(左然后右,反之亦然),那么如果你有足够多的吃迭代次数,死锁几乎是肯定的。

  • 说明:*

让我们假设每个哲学家先锁定左互斥锁,然后再锁定右互斥锁。如果它们同时执行此操作,那么它们都将成功获得左互斥量,然后它们都将阻止尝试获得右互斥量。这个案例很容易分析。
当您创建更复杂的锁定模式时,情况变得更复杂,难以分析。当这种情况发生时,分析人员更有可能意外地错过可能发生死锁的场景。在这样的场景中,死锁变得更加罕见也是很典型的。
这不是好事。
最糟糕的情况是,您错误地认为不会发生死锁,而测试并没有揭示出这一点,因为发生死锁的概率非常低。所以你把代码发出去,两年后它就在现场死锁了。调试这样的东西是噩梦的东西。
最好的办法是使用一个万无一失的系统。std::lock就是这样一个同时锁定多个互斥锁的傻瓜系统,或者它的C++17等价物std::scoped_lock<Mutex1, Mutex2, ...>(它只是在其成员函数lock()中调用std::lock)。
std::lock(lck1, lck2)永远不会死锁(除非在没有std::lock的情况下在其他地方锁定这些锁)。
Here is a paper,探索了实现std::lock的各种算法的性能。如果您相信总是以某种预定义的顺序锁定锁是最好的方法,那么这是一本有趣的读物。按顺序锁定是工具箱中的一个很好的工具。然而,如本文所示,它是std::lock的次优算法。
在我写这个答案的时候,我所知道的所有std::lock的实现(MSVC,gcc,llvm)都有一个非常高性能的实现。1因此,当替代方案试图弄清楚锁定多个锁的奇特模式是否有可能导致死锁时,使用std::lock是明智之举。
[1]这篇论文是这样描述“持久”算法的:
据我所知,没有人认真地提出这个算法作为一个好的解决方案。然而,这里包含了它,因为至少有一个std::lock的发布实现实现了这个精确的算法。
自本文撰写以来的九年中,这些实现有了很大的改进。

相关问题