原子的正确用法
Correct usage of atomics
我已经编写了一个基于向量的小型轻量级推送/弹出队列(认为它应该很快),如下所示:
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;
};
矢量的大小应该是已知的,并且我想知道为什么它不安全?在什么情况下,这会扰乱我的结构?
您的start
和end
是原子变量,但使用std::vector::operator[]
不是原子操作,因此它不是线程安全的。
假设您有10个线程,vector
的大小是5。现在,假设它们都在执行,比如push
。
现在假设所有10个线程可能都通过了检查,并且if (end==my_queue.size())
被评估为false
,因为end
没有达到限制,这意味着vector
没有满。
然后,它们可能都递增end
,同时调用std::vector::operator[]
。至少有5个线程将尝试访问向量"外部"的元素。
您使用operator[]
来推送项目,但这不会为了添加项目而增加向量。因此,当试图将一个项添加到不存在的索引时,您将得到未定义的行为(可能还有访问冲突)。
此外,尽管您在start
和end
上使用原子操作,但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;
}
尽管乍一看,这段代码在许多帐户上看起来都是错误的,但实际上它只包含一个问题。否则,在其限制范围内,它是完全好的。
您正在创建一个初始化为某个特定大小的vector
,并且不允许推送超过该给定大小的元素。这有点"奇怪的行为",但如果这是所希望的,那么就没有问题了。
调用vector::operator[]
对于线程安全来说看起来非常麻烦,因为它不是一个原子操作,但它本身并不是一个问题。vector::operator[]
所做的只是返回对与您提供的索引相对应的元素的引用。它不做任何边界检查或重新分配,也不做任何其他复杂的事情,可能会在并发的情况下破坏这些事情(很可能,它可以归结为一条LEA指令)
在任何情况下都使用fetch_add
,这是确保索引在线程之间唯一的正确心态。如果索引是唯一的,那么就不会有两个线程访问同一个元素,因为这种访问是否是原子访问并不重要。即使几个线程同时原子地递增一个计数器,它们也会得到不同的结果(即,不会"丢失"任何增量)。至少这是理论,就push
而言也是如此。
真正的问题在于pop
函数,其中(除了push
之外)在比较局部变量和调用operator[]
之前,您没有将索引(或者更正确地说,两个索引)原子化地fetch_add
到局部变量中
这是一个问题,因为if(start==end)
本身不是原子的,并且在几行之后调用operator[]
时也不是原子的。您必须获取两个值才能在一个原子操作中进行比较,否则您无法断言比较在任何方面都是有意义的。否则:
- 当您比较
start
或end
(或两者)时,另一个线程(或另外两个或三个线程)可以递增它们,您对是否已达到最大容量的检查将失败 - 在检查之后,另一个线程可能会增加
start
,但在看到operator[]
调用中的fetch_add
之前,这会导致索引越界并调用未定义的行为
这里需要注意的是,尽管使用原子操作做了"正确的事情",但程序仍然不会正确运行,因为不仅仅单个操作需要是原子操作。
- C++中原子的替代品<variant>
- 调用原子的 store() 时可以调用基类型类的函数吗?C++
- 为什么 C++20 不使用"requires"来限制原子的 T<T>?
- C++ 中函数中 Const 用法之间的差异
- boost::asio::io_service::p ost 是原子的吗?
- 原子的矢量完全线程安全?
- Clang vs GCC:枚举用法中的单冒号
- std::包含原子的类的向量
- 初始化原子指针是原子的吗?如果初始化或内存分配引发,会发生什么情况?
- 标准::原子的锁在哪里
- stat() 相对于文件系统是原子的
- 试图最大程度地减少对每次迭代的原子的检查
- 原子的类内初始化
- 对 c++ 11 原子变量的操作实际上是原子的
- 如何配置 g++,以便 x++ 是原子的(Ubuntu,openmp)
- Qt的事件循环线程是安全的还是原子的?处理"队列连接"时如何同步?
- writev() 真的是原子的吗?
- 当某些错误可以接受时,顺序加载存储原子的内存顺序应该是什么
- 原子的正确用法
- 无锁堆栈-这是c++11宽松原子的正确用法吗?它能被证明吗