【问题标题】:Avoid race condition using std::mutex使用 std::mutex 避免竞争条件
【发布时间】:2017-12-02 23:50:43
【问题描述】:

我正在使用 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;          
    }
}

我的解决方案好吗?或者我只是想多了,我朋友告诉我的第一个解决方案就足够了?有没有其他解决方案?该解决方案可以像我建议的那样避免“原子事务”。

【问题讨论】:

  • 为什么你认为如果你从线程中调用pop/push,就会出现竞争条件?线程 A 调用 push 并阻塞 mutex,线程 B 调用 pop 并等待 mutex 被 push 解除阻塞。
  • 您正在尝试解决一个非问题。 “mutex aka 互斥在这里只确保单个功能不在一起。” --- 这是错误的假设。
  • 按照您编写fun1fun2 的方式,永远不会存在竞争条件 --- 您通过值(即副本)而不是通过引用传递对象。跨度>

标签: c++ multithreading mutex


【解决方案1】:

一个给定的互斥锁是一个单一的锁,可以在任何时候被一个线程持有。

如果一个线程 (T1) 在 push() 中持有给定对象的锁,另一个线程 (T2) 无法在 pop() 中获取它,并且将被阻塞直到 T1 释放它。在释放 T2 的那个点(或同样被同一个互斥锁阻塞的另一个线程)将被解除阻塞并允许继续。

您无需在一个成员中完成所有锁定和解锁操作。

如果它们出现在消费者代码中,您可能仍会引入 竞态条件 的地方是这样的构造:

if(!stack.empty()){
    auto item=stack.pop();//Guaranteed?
}

如果另一个线程 T2 在线程 T1 进入 empty()(上图)后进入 pop() 并在互斥体上等待时被阻塞,则 T1 中的 pop() 可能会失败,因为 T2 '首先到达那里'。在该 sn-p 中,在 empty() 的结尾和 pop() 的开头之间可能会发生任意数量的操作,除非其他同步正在处理它。

在这种情况下,您应该想象 T1 和 T2 从字面上 竞速pop(),当然它们可能会与不同的成员竞速并且仍然相互无效...

如果你想构建这样的代码,你通常必须添加更多的 atomic 成员函数,如 try_pop(),如果堆栈为空,则返回(比如说)一个空的 std::shared_ptr&lt;&gt;

我希望这句话不会让人困惑:

在成员函数中锁定对象互斥体可以避免竞争 条件 between 调用这些成员函数,但不 in 在对这些成员函数的调用之间

解决此问题的最佳方法是添加执行多个“逻辑”操作的“复合”函数。这往往与良好的类设计背道而驰,在这种设计中,您设计了一组逻辑上的最小操作,而消费代码将它们组合在一起。

另一种方法是允许消费代码访问互斥锁。例如公开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();
}

在每个成员的开头添加互斥体并获取它只是故事的开始......

脚注:希望这能解释 mutual exlusion (mut****ex)。还有一个关于内存障碍的其他主题潜伏在表面之下,但如果你以这种方式使用互斥锁,你现在可以将其视为实现细节......

【讨论】:

  • 谢谢。我现在明白了。
【解决方案2】:

你误会了什么。你不需要那个changeStack 函数。

如果您忘记了lock_guard,它的外观如下(使用lock_guard,代码的作用相同,但lock_guard 更方便:自动解锁):

push() {
  m.lock();
  // do the push
  m.unlock();
}

pop() {
  m.lock();
  // do the pop
  m.unlock();
}

push 被调用时,互斥量将被锁定。现在,想象一下,在其他线程上,调用了 poppop 尝试锁定互斥体,但它无法锁定它,因为 push 已经锁定它。所以它必须等待push 解锁互斥锁。当push 解锁互斥锁时,pop 可以锁定它。

所以,简而言之,是 std::mutex 进行互斥,而不是 lock_guard

【讨论】:

  • 只是一个问题...lock_guard 不是比使用lockunlock 更好/更安全,因为如果pushpop 抛出,至少是析构函数的lock_guard 会自动解锁互斥锁吗?
  • @ArdaAytekin:是的,lock_guard 是首选。我以这种方式制作了这个例子,因为它更容易解释。更容易看到发生了什么(无需解释函数返回时的“隐形”解锁)
猜你喜欢
  • 1970-01-01
  • 2015-01-30
  • 2010-09-25
  • 2019-01-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-25
  • 2019-06-12
相关资源
最近更新 更多