简体   繁体   English

使用std :: mutex避免竞争条件

[英]Avoid race condition using std::mutex

I am dealing with the multi-threading project with C++ and I doubt about std::mutex 我正在使用C ++处理多线程项目,我对std :: mutex表示怀疑

Let's assume that I have a stack. 假设我有一个堆栈。

#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();
    }
};

Someone said that using this stack can avoid race condition. 有人说使用此堆栈可以避免竞争情况。 However I think that problem here is that mutex aka mutual exclusion here only ensure for individual function not together. 但是,我认为这里的问题是互斥锁(也称为互斥)仅确保单个功能不能一起使用。 For example, I can have the threads call push and pop. 例如,我可以将线程称为push和pop。 Those function still have problem of race condition. 这些功能仍然存在比赛条件问题。

For example: 例如:

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);

    //
}

If a thread fun1 and fun2 call the same stack (global variable for simple). 如果线程fun1和fun2调用同一堆栈(为简单起见,全局变量)。 So it can be a race condition(?) 所以这可能是比赛条件(?)

I have only solution I can think is using some kind of atomic transaction means instead of calling directly push(), pop(), empty(), I call them via a function with a "function pointer" to those function and with only one mutex. 我只有一个可以想到的解决方案,是使用某种原子事务处理方式,而不是直接调用push(),pop(),empty(),而是通过一个带有“函数指针”的函数调用这些函数,并且只有一个互斥体。

For example: 例如:

#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;          
    }
}

Is my solution good? 我的解决方案好吗? Or I just overthinking and the first solution my friend told me is good enough? 还是我只是想得太多而我的朋友告诉我的第一个解决方案就足够了? Are there any other solution for this? 还有其他解决方案吗? The solution can avoid "atomic transaction" like I suggest. 该解决方案可以避免像我建议的“原子交易”。

A given mutex is a single lock and can be held by a single thread at any one time. 给定的互斥锁是单个锁,并且可以随时由单个线程持有。

If a thread (T1) is holding the lock on a given object in push() another thread (T2) cannot acquire it in pop() and will be blocked until T1 releases it. 如果线程(T1)在push()持有给定对象的锁,则另一个线程(T2)无法在pop()获取该锁,并且该线程将被阻塞,直到T1释放它。 At that point of release T2 (or another thread also blocked by the same mutex) will be unblocked and allowed to proceed. 在发布的那一刻,T2(或也被同一互斥锁阻止的另一个线程)将被解除阻止并继续执行。

You do not need to do all the locking and unlocking in one member. 您无需在一个成员中进行所有锁定和解锁。

The point where you may still be introducing a race condition is constructs like this if they appear in consumer code: 如果它们出现在使用者代码中,那么您可能仍会引入竞争条件的地方是这样的结构:

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

If another thread T2 enters pop() after thread T1 enters empty() (above) and gets blocked waiting on the mutex then the pop() in T1 may fail because T2 'got there first'. 如果另一个线程T2在线程T1输入empty() (在上方pop()之后进入pop()并在互斥对象上等待阻塞,则T1中的pop()可能会失败,因为T2“先到达那里”。 Any number of actions might take place between the end of empty() and the start of pop() in that snippet unless other synchronization is handling it. 在该代码段的empty()结束与pop()的开始之间可能发生任何数量的操作,除非其他同步正在处理它。

In this case you should imagine T1 & T2 literally racing to pop() though of course they may be racing to different members and still invalidate each other... 在这种情况下,您应该想象T1和T2确实竞速pop()尽管它们当然可能竞速到不同的成员,但仍然彼此无效...

If you want to build code like that you usually have to then add further atomic member functions like try_pop() which returns (say) an empty std::shared_ptr<> if the stack is empty. 如果要构建这样的代码,通常必须添加其他原子成员函数,例如try_pop() ,如果堆栈为空,则该函数返回(例如)一个空的std::shared_ptr<>

I hope this sentence isn't confusing: 我希望这句话不要混淆:

Locking the object mutex inside member functions avoids race conditions between calls to those member functions but not in between calls to those member functions. 将对象互斥锁锁定在成员函数内部可避免调用这些成员函数之间发生竞争但不会调用这些成员函数之间发生竞争。

The best way to solve that is by adding 'composite' functions that are doing the job of more than one 'logical' operation. 解决此问题的最佳方法是添加执行多个“逻辑”操作的“复合”功能。 That tends to go against good class design in which you design a logical set of minimal operations and the consuming code combines them. 这往往与良好的类设计背道而驰,在类设计中,您设计一组逻辑上最少的操作,而消耗代码将它们组合在一起。

The alternative is to allow the consuming code access to the mutex. 另一种选择是允许使用方代码访问互斥量。 For example expose void lock() const; 例如暴露void lock() const; and void unlock() cont; void unlock() cont; members. 成员。 That is usually not preferred because (a) it becomes very easy for consumer code to create deadlocks and (b) you either use a recursive lock (with its overhead) or double up member functions again: 这通常不是可取的,因为(a)消费者代码很容易创建死锁,并且(b)您可以使用递归锁(及其开销)或再次加倍成员函数:

void pop(); //Self locking version...
void pop_prelocked(); //Caller must hold object mutex or program invalidated.

Whether you expose them as public or protected or not that would make try_pop() look something like this: 无论你将它们公开为publicprotected与否,这将使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();
}

Adding a mutex and acquiring it at the start of each member is only the start of the story... 添加互斥锁并在每个成员的开头获取它只是故事的开始...

Footnote: Hopefully that explains mu tual ex lusion ( mut****ex ). 脚注:希望解释图阿尔 lusion(MUT ****前 )。 There's a whole other topic round memory barriers lurking below the surface here but if you use mutexes in this way you can treat that as an implementation detail for now... 这里还有其他主题围绕着内存障碍 ,但是如果您以这种方式使用互斥锁,则可以将其视为实现细节...

You misunderstand something. 你误会了。 You don't need that changeStack function. 您不需要那个changeStack函数。

If you forget about lock_guard , here's what it looks like (with lock_guard , the code does the same, but lock_guard makes it convenient: makes unlock automatic): 如果您忘记了lock_guard ,则外观如下所示(使用lock_guard ,代码执行相同的操作,但是lock_guard使其变得很方便:使解锁自动进行):

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

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

When push is called, mutex will be locked. 调用push ,互斥锁将被锁定。 Now, imagine, that on other thread, there is pop called. 现在,想象一下,在其他线程上有pop调用。 pop tries to lock the mutex, but it cannot lock it, because push already locked it. pop尝试锁定互斥锁,但无法将其锁定,因为push已将其锁定。 So it has to wait for push to unlock the mutex. 因此,它必须等待push以解锁互斥锁。 When push unlocks the mutex, then pop can lock it. push解锁互斥锁时, pop可以将其锁定。

So, in short, it is std::mutex which does the mutual exclusion, not the lock_guard . 简而言之,是std::mutex ,而不是lock_guard

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM