通读本书,它说明了如何为operator*
实施更复杂的操作,例如std::atomic<T>
。实现使用compare_exchange_weak
,我想我了解它是如何工作的。现在,我自己实现了一些东西,看看。
#include <type_traits>
#include <atomic>
#include <iostream>
/*template <typename T, typename = std::enable_if_t<std::is_integral<T>::value>>
std::atomic<T>& operator*=(std::atomic<T>& t1, T t2) {
T expected = t1.load();
while(!t1.compare_exchange_weak(expected, expected * t2))
{}
return t1;
}*/
template <typename T, typename = std::enable_if_t<std::is_integral<T>::value>>
std::atomic<T>& operator*=(std::atomic<T>& t1, T t2) {
T expected = t1.load();
t1.compare_exchange_weak(expected, expected * t2);
return t1;
}
int main() {
std::atomic<int> t1 = 5;
std::atomic<int> t2;
t2 = (t1 *= 5).load();
std::cout << "Atomic t1: " << t1 << "\n";
std::cout << "Atomic t2: " << t2 << "\n";
}
我有两个版本的代码,本书的版本已被注释掉。我不明白为什么我应该等待一个繁忙的循环来执行原子compare_exchange
。在我的版本中,我只是单独调用它,然后查看Godbolt中生成的程序集,两者都使用
lock cmpxchg dword ptr [rsp + 8], ecx
,看起来和我非常相似。那么,为什么我需要一个像书中那样的等待循环来使这件事原子化?我的版本还可以正常运行吗?
答案 0 :(得分:6)
想象一下在调用load
和compare_exchange_weak
之间,该值被另一个线程更改了。 expected
不再具有当前值。
compare_exchange_weak
的工作方式如下:
以原子方式比较(对象表示(直到C ++ 20)/值 * this的表示形式(自C ++ 20起)与预期的表示形式,如果 那些按位相等,用所需的替换前者(执行 读取-修改-写入操作)。否则,加载存储的实际值 在* this中达到预期(执行加载操作)。 cppreference
根据以上t1
的描述,将不会更改,并且不会存储您的乘法。
通过循环,您可以确保更新t1
并存储乘法结果,或者更新expected
并在循环的下一次迭代中重试(循环只会在第一种情况发生后才停止)
编辑:
您可以通过模拟并发访问来“尝试”它。在交换结果之前,会有另一个线程进入并更改atomic的值。在下文中,compare_exchange_weak
仅影响expected
。
+----------- Thread 1 -----------+---------- Thread 2 ----------+
| ex = t1.load() | |
| | t1.store(42) |
| t1.cmp_xchg_w(ex, ex * t2) | |
此代码模拟并发访问并使单个线程进入睡眠状态。
#include <type_traits>
#include <atomic>
#include <iostream>
#include <chrono>
#include <thread>
template <typename T, typename = std::enable_if_t<std::is_integral<T>::value>>
std::atomic<T>& operator*=(std::atomic<T>& t1, T t2) {
using namespace std::chrono_literals;
T expected = t1.load();
std::this_thread::sleep_for(400ms);
t1.compare_exchange_weak(expected, expected * t2);
return t1;
}
int main() {
std::atomic<int> t1 = 5;
std::atomic<int> t2;
std::thread th1([&](){
t2 = (t1 *= 5).load();
});
std::thread th2([&](){
using namespace std::chrono_literals;
std::this_thread::sleep_for(100ms);
t1.store(8);
});
th1.join();
th2.join();
std::cout << "Atomic t1: " << t1 << "\n";
std::cout << "Atomic t2: " << t2 << "\n";
}