[英]Should I use std::atomic or std::mutex to avoid the thread race?
[英]Avoid race condition using std::mutex
我正在使用C ++处理多线程项目,我对std :: mutex表示怀疑
假设我有一个堆栈。
#include <exception>
#include <memory>
#include <mutex>
#include <stack>
struct empty_stack: std::exception
{
const char* what() const throw();
};
template<typename T>
class threadsafe_stack
{
private:
std::stack<T> data;
mutable std::mutex m;
public:
threadsafe_stack(){}
threadsafe_stack(const threadsafe_stack& other)
{
std::lock_guard<std::mutex> lock(other.m);
data=other.data;
}
threadsafe_stack& operator=(const threadsafe_stack&) = delete;
void push(T new_value)
{
std::lock_guard<std::mutex> lock(m);
data.push(new_value);
}
std::shared_ptr<T> pop()
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack();
std::shared_ptr<T> const res(std::make_shared<T>(data.top()));
data.pop();
return res;
}
void pop(T& value)
{
std::lock_guard<std::mutex> lock(m);
if(data.empty()) throw empty_stack();
value=data.top();
data.pop();
}
bool empty() const
{
std::lock_guard<std::mutex> lock(m);
return data.empty();
}
};
有人说使用此堆栈可以避免竞争情况。 但是,我认为这里的问题是互斥锁(也称为互斥)仅确保单个功能不能一起使用。 例如,我可以将线程称为push和pop。 这些功能仍然存在比赛条件问题。
例如:
threadsafe_stack st; //global varibale for simple
void fun1(threadsafe_stack st)
{
std::lock_guard<std::mutex> lock(m);
st.push(t);
t = st.pop();
//
}
void fun2(threadsafe_stack st)
{
std::lock_guard<std::mutex> lock(m);
T t,t2;
t = st.pop();
// Do big things
st.push(t2);
//
}
如果线程fun1和fun2调用同一堆栈(为简单起见,全局变量)。 所以这可能是比赛条件(?)
我只有一个可以想到的解决方案,是使用某种原子事务处理方式,而不是直接调用push(),pop(),empty(),而是通过一个带有“函数指针”的函数调用这些函数,并且只有一个互斥体。
例如:
#define PUSH 0
#define POP 1
#define EMPTY 2
changeStack(int kindOfFunction, T* input, bool* isEmpty)
{
std::lock_guard<std::mutex> lock(m);
switch(kindOfFunction){
case PUSH:
push(input);
break;
case POP:
input = pop();
break;
case EMPTY:
isEmpty = empty();
break;
}
}
我的解决方案好吗? 还是我只是想得太多而我的朋友告诉我的第一个解决方案就足够了? 还有其他解决方案吗? 该解决方案可以避免像我建议的“原子交易”。
给定的互斥锁是单个锁,并且可以随时由单个线程持有。
如果线程(T1)在push()
持有给定对象的锁,则另一个线程(T2)无法在pop()
获取该锁,并且该线程将被阻塞,直到T1释放它。 在发布的那一刻,T2(或也被同一互斥锁阻止的另一个线程)将被解除阻止并继续执行。
您无需在一个成员中进行所有锁定和解锁。
如果它们出现在使用者代码中,那么您可能仍会引入竞争条件的地方是这样的结构:
if(!stack.empty()){
auto item=stack.pop();//Guaranteed?
}
如果另一个线程T2在线程T1输入empty()
(在上方pop()
之后进入pop()
并在互斥对象上等待阻塞,则T1中的pop()
可能会失败,因为T2“先到达那里”。 在该代码段的empty()
结束与pop()
的开始之间可能发生任何数量的操作,除非其他同步正在处理它。
在这种情况下,您应该想象T1和T2确实竞速到pop()
尽管它们当然可能竞速到不同的成员,但仍然彼此无效...
如果要构建这样的代码,通常必须添加其他原子成员函数,例如try_pop()
,如果堆栈为空,则该函数返回(例如)一个空的std::shared_ptr<>
。
我希望这句话不要混淆:
将对象互斥锁锁定在成员函数内部可避免在调用这些成员函数之间发生竞争,但不会在调用这些成员函数之间发生竞争。
解决此问题的最佳方法是添加执行多个“逻辑”操作的“复合”功能。 这往往与良好的类设计背道而驰,在类设计中,您设计一组逻辑上最少的操作,而消耗代码将它们组合在一起。
另一种选择是允许使用方代码访问互斥量。 例如暴露void lock() const;
和void unlock() cont;
成员。 这通常不是可取的,因为(a)消费者代码很容易创建死锁,并且(b)您可以使用递归锁(及其开销)或再次加倍成员函数:
void pop(); //Self locking version...
void pop_prelocked(); //Caller must hold object mutex or program invalidated.
无论你将它们公开为public
或protected
与否,这将使try_pop()
是这个样子:
std::shared_ptr<T> try_pop(){
std::lock_guard<std::mutex> guard(m);
if(empty_prelocked()){
return std::shared_ptr<T>();
}
return pop_prelocked();
}
添加互斥锁并在每个成员的开头获取它只是故事的开始...
脚注:希望解释亩图阿尔前 lusion(MUT ****前 )。 这里还有其他主题围绕着内存障碍 ,但是如果您以这种方式使用互斥锁,则可以将其视为实现细节...
你误会了。 您不需要那个changeStack
函数。
如果您忘记了lock_guard
,则外观如下所示(使用lock_guard
,代码执行相同的操作,但是lock_guard
使其变得很方便:使解锁自动进行):
push() {
m.lock();
// do the push
m.unlock();
}
pop() {
m.lock();
// do the pop
m.unlock();
}
调用push
,互斥锁将被锁定。 现在,想象一下,在其他线程上有pop
调用。 pop
尝试锁定互斥锁,但无法将其锁定,因为push
已将其锁定。 因此,它必须等待push
以解锁互斥锁。 当push
解锁互斥锁时, pop
可以将其锁定。
简而言之,是std::mutex
,而不是lock_guard
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.