使用互斥会挂起程序

Use of Mutex hangs the program

本文关键字:挂起 程序      更新时间:2023-10-16

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

我用push((,pop((,top((和empty((方法实现了一个基本的堆栈类。

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

首先,我尝试在不使用互斥锁的情况下实现它,输出是乱码,最终导致段错误,这是意料之中的,因为操作不是原子的,所以数据竞争是不可避免的。

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

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

#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 << "Donen";
return 0;
}

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

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

它给出了乱码输出和段错误。

在当前的同步方案下,仍然可能会给你段错误,即使你使用建议的 RAII 样式锁定

,如下所示:
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;
}

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

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

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

在这些情况下,您需要小心查看同步性方面需要保护的全部范围。这里坏的东西叫原子性,意思是"不能在中间切"的属性。正如你在这里看到的,它说">原子性通常是通过相互排斥来强制执行的"——就像你一样使用互斥体。缺少的是它的粒度太细——"原子的大小"操作太小了。你应该保护整个empty()序列 -top()-pop()作为一个整体,因为我们现在意识到我们不能将任何部分从三者中分离出来。在代码中,它可能看起来像在func()内部调用 this 并仅在返回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;
}

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

一个错误是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";
}
}