我正在使用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(),我通过带有&#34;函数指针的函数调用它们#34 ;这些功能只有一个互斥锁。
例如:
#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;
}
}
我的解决方案好吗?或者我只是过度思考,我的朋友告诉我的第一个解决方案是否足够好?还有其他解决方案吗?该解决方案可以避免原子事务&#34;就像我建议的那样。
答案 0 :(得分:2)
互斥锁是单个锁,任何时候都可以由一个线程保留。 它没有按功能运作。
因此,如果一个线程(T1)持有push()
中给定对象的锁,则另一个线程(T2)无法在pop()
中获取它,并且在T1释放它之前将被阻塞。在释放时,T2(或另一个也被相同互斥锁阻塞的线程)将被解锁并允许继续。
您无需在一个成员中执行所有锁定和解锁操作。
如果它们出现在消费者代码中,那么您可能仍在引入竞争条件的结构是这样的结构:
if(!stack.empty()){
auto item=stack.pop();//Guaranteed?
}
如果另一个线程T2在线程T1进入pop()
(上面)之后进入empty()
并且在互斥锁上等待被阻塞,那么T1中的pop()
可能会失败,因为T2&#39;第一个&#39;。除非其他同步正在处理,否则可能会在empty()
的结尾与该代码段中pop()
的开头之间发生任意数量的操作。
在这种情况下你应该想象T1&amp; T2字面上赛车到pop()
虽然当然他们可能会竞争不同的成员但仍然互相失效......
如果你想构建那样的代码,你通常需要添加更多 atomic 成员函数,如try_pop()
,如果堆栈是,则返回(比如说)空std::shared_ptr<>
空。
我希望这句话不会令人困惑:
锁定成员函数内的对象互斥体可避免竞争 之间的条件调用这些成员函数但不调用 in 在之间调用这些成员函数。
解决这个问题的最佳方法是添加&#39;复合&#39;正在完成多个逻辑工作的功能。操作。这往往违背良好的类设计,在这种设计中,您设计了一组逻辑的最小操作,消费代码将它们组合在一起。
另一种方法是允许使用代码访问互斥锁。例如,公开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();
}
添加互斥锁并在每个成员的开头获取它只是故事的开始......
脚注:希望这能解释 mu tual ex lusion( mut **** ex )。还有一个完整的其他主题围绕在表面下方的成员障碍,但如果你以这种方式使用互斥锁,你现在可以将其视为一个实现细节......
答案 1 :(得分:1)
你误会了什么。您不需要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
。