使用 Mutex 挂起程序

Use of Mutex hangs the program

我正在尝试学习 C++ 中的并发编程。

我使用 push()、pop()、top() 和 empty() 方法实现了一个基本堆栈 class。

我创建了两个线程,它们都将尝试访问顶部元素并将其弹出,直到堆栈变空。

首先,我尝试在不使用互斥锁的情况下实现它,输出是乱码,最后导致 segfault,这是预期的,因为操作不是原子的,所以数据竞赛是不可避免的。

所以我尝试用互斥量实现它,程序挂了,甚至没有给出任何输出,因为没有解锁互斥量。

现在,我已经正确地使用了互斥锁锁定+解锁序列,我的程序正在根据需要提供正确的输出,但之后程序挂起——可能是由于线程仍在执行或控制未到达到主线程?

#include <thread>
#include <mutex>
#include <string>
#include <iostream>
#include <vector>

using std::cin;
using std::cout;
std::mutex mtx;
std::mutex a_mtx;


class MyStack
{
    std::vector<int> stk;
public:
    void push(int val) {
        stk.push_back(val);
    }

    void pop() {
        mtx.lock();
        stk.pop_back();
        mtx.unlock();
    }

    int top() const {
        mtx.lock();
        return stk[stk.size() - 1];
    }

    bool empty() const {
        mtx.lock();
        return stk.size() == 0;
    }
};

void func(MyStack& ms, const std::string s)
{
    while(!ms.empty()) {
        mtx.unlock();
        a_mtx.lock();
        cout << s << " " << ms.top() << "\n";
        a_mtx.unlock();
        mtx.unlock();
        ms.pop();
    }

    //mtx.unlock();
}

int main(int argc, char const *argv[])
{
    MyStack ms;

    ms.push(3);
    ms.push(1);
    ms.push(4);
    ms.push(7);
    ms.push(6);
    ms.push(2);
    ms.push(8);

    std::string s1("from thread 1"), s2("from thread 2");
    std::thread t1(func, std::ref(ms), "from thread 1");
    std::thread t2(func, std::ref(ms), "from thread 2");

    t1.join();
    t2.join();

    cout << "Done\n";

    return 0;
}

我想是因为一旦堆栈为空,我就没有解锁互斥锁。因此,当我取消注释注释行并 运行 它时,它会给出乱码输出和段错误。

我不知道我哪里做错了。这是编写线程安全堆栈的正确方法吗class?

一个错误是 MyStack::topMyStack::empty 它没有解锁互斥量。

使用std::lock_guard<std::mutex>自动解锁互斥量并消除此类意外死锁的风险。例如:

bool empty() const {
    std::lock_guard<std::mutex> lock(mtx);
    return stk.empty();
}

它可能还需要在 MyStack::push 中锁定互斥体。


另一个错误是方法级别的锁定粒度太细,empty() 后跟 top()pop() 不是原子的。

可能的修复:

class MyStack
{
    std::vector<int> stk;
public:
    void push(int val) {
        std::lock_guard<std::mutex> lock(mtx);
        stk.push_back(val);
    }

    bool try_pop(int* result) {
        bool popped;
        {
            std::lock_guard<std::mutex> lock(mtx);
            if((popped = !stk.empty())) {
                *result = stk.back();
                stk.pop_back();
            }
        }
        return popped;
    }
};

void func(MyStack& ms, const std::string& s)
{
    for(int top; ms.try_pop(&top);) {
        std::lock_guard<std::mutex> l(a_mtx);
        cout << s << " " << top << "\n";
    }
}

it gives gibberish output and segfault.

它将仍然潜在地给你segfault在当前的同步方案下,即使你像这样使用建议的 RAII style locking:

void pop() {
    std::lock_guard<std::mutex> lock{ mtx };
    stk.pop_back();
}

int top() const {
    std::lock_guard<std::mutex> lock{ mtx };
    return stk[stk.size() - 1];
}

bool empty() const {
    std::lock_guard<std::mutex> lock{ mtx };
    return stk.size() == 0;
}

因为 您没有处理不同线程对这些方法的两次后续调用之间出现的 race-condition。例如,想一想当堆栈还剩一个元素时会发生什么,一个线程询问它是否为空并得到一个 false 然后你有一个上下文切换而另一个线程得到相同的 false同样的问题。所以 他们都在为 top()pop() 比赛。虽然第一个已经弹出它然后另一个尝试 top() 它会在 stk.size() - 1 产生 -1 的情况下这样做。因此,你会得到一个 segfault 来尝试访问一个不存在的堆栈负索引:(

I don't know where I am doing a mistake. Is this the right way of writing a thread-safe stack class?

不,这不是正确的方法,互斥体只保证锁定在同一个互斥体上的其他线程当前不能运行同一段代码。如果他们到达同一个部分,他们将被阻止进入该部分,直到互斥体被释放。但是 你根本没有在 empty() 的调用和其余调用之间锁定。一个线程获取到empty(),加锁,获取值,然后释放,然后另一个线程自由进入查询,很可能获取到相同的值。是什么阻止了它稍后输入对 top() 的调用,又是什么阻止了第一个线程当时已经在同一个 pop() 之后?

在这些情况下,您需要仔细查看同步性方面需要保护的全部范围。这里打破的东西叫做atomicity,意思是"not being able be cut in middle"的属性。如您所见,here it says that "Atomicity is often enforced by mutual exclusion," -- as in by using mutexes, like you did. What was missing is that it was grained 太细了——"size of the atomic" 操作太小了。你应该一直保护 empty()-top()-pop() 的整个序列作为一个整体,因为我们现在意识到我们不能将三者中的任何部分分开。在代码中,它看起来像是在 func() 内部调用它,并且仅当它返回 true:

时才打印到 cout
bool safe_pop(int& value)
{
    std::lock_guard<std::mutex> lock{ mtx };

    if (stk.size() > 0)
    {
        value = stk[stk.size() - 1];
        stk.pop_back();
        return true;
    }

    return false;
}

诚然,这并没有为这里的并行工作留下多少,但我想这是一个不错的并发练习。