正确使用原子

时间:2014-01-15 14:12:24

标签: c++ multithreading c++11 atomicity

我写了一个基于矢量的小型轻量级推/弹队列(想象它应该很快),如下所示:

template <typename T> class MyVectorQueue{

public:

    MyVectorQueue (size_t sz):my_queue(sz),start(0),end(0){}

    void push(const T& val){
       size_t idx=atomic_fetch_add(&end,1UL);
       if (idx==my_queue.size())
          throw std::runtime_error("Limit reached, initialize to larger vector");
       my_queue[idx]=val;
    }

    const T& pop(){
        if (start==end) 
           return end__;
        return my_queue[start.fetch_add(1UL)];
    }

    size_t empty()
    {
        return end==start;
    }

    const T& end() {
        return end__;
    }

private:
    T end__;
    std::atomic<size_t> start,end;
    std::vector<T> my_queue;    
}; 

矢量的大小应该是已知的 我想知道为什么它不是线程安全的?在什么情况下这会弄乱我的结构?

3 个答案:

答案 0 :(得分:6)

您的startend是原子变量,但使用std::vector::operator[]不是原子操作,使其不是线程安全的。


假设您有10个线程,vector的大小为5.现在,假设所有线程都在执行,比如说push

现在假设所有10个线程都已通过检查,if (end==my_queue.size())评估为false,因为end尚未达到限制,这意味着 - vector不满。

然后,所有这些都可能会增加end并同时调用std::vector::operator[]。至少有5个线程将​​尝试访问向量之外的元素。

答案 1 :(得分:1)

您正在使用operator[]来推送项目,但这不会增加向量以添加项目。因此,当您尝试将项添加到不存在的索引时,您将获得未定义的行为(可能还有访问冲突)。

此外,虽然您在startend上使用原子操作,但vector不是原子操作。因此,例如,您可以让多个线程调用push,它们会自动更改end,然后调用operator[],这不是线程安全的。相反,您应该考虑使用互斥锁和std::deque

std::mutex mutex;
std::deque<T> my_queue;

void push(const T& val){
   std::lock_guard<std::mutex> guard(mutex);
   //..code to check if full 
   my_queue.push_back(val);
}

const T& pop(){
    std::lock_guard<std::mutex> guard(mutex);
    //code to check if empty and that start index does not pass end index
    T item=my_queue.front();
    my_queue.pop_front();
    return item;
}

答案 2 :(得分:1)

虽然乍一看这个代码在许多帐户上看起来很危险,但它实际上只包含一个问题。否则,在其限制范围内完全没问题。

您正在创建一个初始化为某个特定大小的vector,并且您不允许推送比此给定大小更多的元素。这有点“奇怪的行为”,但如果这是所期望的,那么就没有问题了。

调用vector::operator[]对于线程安全看起来非常麻烦,因为它不是原子操作,但实际上并不是问题。 vector::operator[]所做的只是返回对与您提供的索引相对应的元素的引用。它不执行任何边界检查或重新分配或任何其他复杂的东西可能在并发存在时中断(很可能,它归结为类似于单个LEA指令)。
您在每种情况下都使用fetch_add,这是保证索引在线程中唯一的正确思维方式。如果索引是唯一的,那么没有两个线程可以访问同一个元素,只要该访问是否是原子的并不重要。即使几个线程同时以原子方式递增计数器,它们也会得到不同的结果(即没有增量在途中“丢失”)。至少那是理论,到目前为止push也是如此。

一个真正的问题在于pop函数,其中(除了push之外)你没有原子地fetch_add索引(或更准确地说, 两个指数)在比较它们和调用operator[]之前进入局部变量 这是一个问题,因为if(start==end)本身并不是原子的,并且它与后面几行调用operator[]并不是原子的。您必须获取两个值才能在一个原子操作中进行比较,或者您无法以任何方式断言比较有意义。否则:

  • 在比较它们时,另一个线程(或另外两个或三个线程)可以递增startend(或两者),并且检查已达到最大容量将失败。
  • 在检查之后,另一个线程可能会增加start,但在看到fetch_add的{​​{1}}内部之前,会导致您索引越界并调用未定义的行为。

这里要注意的非直观的事情是虽然你通过使用原子操作做“正确的事”,但程序仍然不能正常运行,因为不仅仅是个人操作需要是原子的。