【问题标题】:I dont see the race condition here?我没有看到这里的比赛条件?
【发布时间】:2014-08-10 15:15:37
【问题描述】:

这是书中的示例代码:

#include <exception>
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(std::move(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>(std::move(data.top())));
            data.pop();
            return res;
        }

        void pop(T& value)
        {
            std::lock_guard<std::mutex> lock(m);
            if(data.empty()) throw empty_stack();
            value=std::move(data.top());
            data.pop();
        }

        bool empty() const
        {
            std::lock_guard<std::mutex> lock(m);
            return data.empty();
        }
};

上面写着:

empty() 和其中一个之间可能存在竞争条件 pop() 函数,但是因为代码显式检查 包含堆栈为空,同时在 pop() 中持有锁,这场比赛 条件没有问题

如果它们都在“全局”互斥锁被锁定时完成,这怎么可能是竞争条件?

【问题讨论】:

  • 可能只是一本写得不好的书?
  • 想一想如果使用该类的人写了例如会发生什么while (!stack.empty()) stack.pop(); 在多个线程中。
  • @JoachimPileborg 所以你是说这两个操作需要在同一个锁中完成? API 看起来不太好——暴露的太多了。
  • 完全正确,除了公开不太理想的互斥体之外,没有其他好的方法。但是,您可以添加一个新函数pop_all,将谓词作为参数,然后pop_all 函数对每个弹出项目调用谓词进行循环和弹出。

标签: c++ multithreading locking


【解决方案1】:

竞争条件在客户端代码中:

threadsafe_stack<int> st;
...
if (!st.empty()) {
   int value = st.pop();    // Kaboom
   //...
}

或者换句话说,另一个线程可能在 empty() 和 pop() 方法调用之间弹出了堆栈。这通常是细粒度锁定不起作用并且需要由客户端程序员处理的原因。最不可能做对的人。

您应该将 pop() 替换为 trypop() 以使其成为线程安全的。

【讨论】:

    【解决方案2】:

    我不喜欢您提供的代码。它看起来对我有误导性。

    首先,并发数据结构没有“大小”或“空虚”。没有像你可以采取行动的可观察属性。始终返回值 true 的函数将与您的“空”函数一样有用。

    您可以对并发数据结构做的唯一明智的事情是尝试从中获取数据。然后你要么成功要么失败,然后你处理它。您的容器为空并不是一个例外情况。您的代码编写方式,用户无法保证没有异常!这是不对的。

    最后,共享指针似乎毫无意义。您的容器在其设计方式中具有独特的消费者,因此界面应该这样做。是否要创建共享指针取决于用户,这与您无关。

    所以,总而言之,我推荐如下界面:

    void push(T const & x)
    {
        std::lock_guard<std::mutex> lock(m);
        data.push(x);
    }
    
    void push(T && x)
    {
        std::lock_guard<std::mutex> lock(m);
        data.push(std::move(x));
    }
    
    bool pop(std::unique_ptr<T> & dst)
    {
        std::lock_guard<std::mutex> lock(m);
    
        if (data.empty()) { return false; }
    
        dst.reset(::new T(std::move(data.top()));
        data.pop();
        return true;
    }
    

    就是这样,公共接口中不应包含任何其他内容(可能除了emplace 类似的推送功能)。 pop函数的细节由你决定;唯一指针允许您拥有不可默认构造的值。您还可以使用dst 的值作为元素是否成功提取的信号,并取消bool

    【讨论】:

    • 我认为这是因为我给出的示例是一本书的一部分,这本书也介绍了糟糕的设计决策——它不是说“这是最好的”,但我真的很喜欢你的回答。跨度>
    • 他们的下一个设计使用了一个条件变量,所以如果你调用 pop() 并且没有任何内容,它会等待......
    • 为什么你的第一个 push() 接受一个 const T 对象,而你的第二个 push() (对于右值)是一个非 const T 对象?因为 std::move() 会导致移动构造函数/赋值运算符为移动改变其数据成员?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-14
    • 1970-01-01
    • 2023-03-20
    • 1970-01-01
    相关资源
    最近更新 更多