网站首页 > 技术文章 正文
std::atomic类封装了一个单独的对象,并保证了它的原子性。对原子对象的写入由内存顺序策略控制,读取可能同时发生。它通常用于同步不同线程之间的访问。
std::atomic通过其模板类型定义了一个原子类型。该类型必须是平凡的。如果一个类型占用连续的内存,没有用户定义的构造函数,并且没有虚成员函数,那么它就是平凡的。所有基本类型都是平凡的。
虽然可以构造一个平凡类型,但std::atomic最常与简单的原始类型一起使用,如bool、int、long、float和double。
如何做……
本示例使用一个简单的函数,该函数循环遍历一个计数器,以演示共享原子对象。我们将生成一群这些循环作为线程,它们共享原子值:
原子对象通常放在全局命名空间中。它们必须对需要共享其值的所有线程都是可访问的:
std::atomic<bool> ready{};
std::atomic<uint64_t> g_count{};
std::atomic_flag winner{};
ready对象是一个bool类型,当所有线程都准备好开始计数时,它被设置为true。
g_count对象是一个全局计数器。它由每个线程递增。
winner对象是一个特殊的atomic_flag类型。它用于指示哪个线程首先完成。
我们使用几个常量来控制线程的数量和每个线程的循环次数:
constexpr int max_count{1000 * 1000};
constexpr int max_threads{100};
我将它设置为运行100个线程,并在每个线程中计数1,000,000次迭代。
每个线程都会生成countem()函数。它循环max_count次,并在每次循环迭代时递增g_count。这是我们使用原子值的地方:
void countem (int id) {
while(!ready) std::this_thread::yield();
for(int i{}; i < max_count; ++i) ++g_count;
if(!winner.test_and_set()) {
std::cout << format("thread {:02} won!\n", id);
}
};
ready原子值用于同步线程。每个线程将调用yield(),直到ready值被设置为true。yield()函数将执行权让给其他线程。
for循环的每次迭代都会递增g_count原子值。最终值应该等于max_count * max_threads。
循环完成后,winner对象的test_and_set()方法用于报告获胜的线程。test_and_set()是atomic_flag类的一个方法。它设置标志并返回设置之前的bool值。
我们在之前已经使用了make_commas()函数。它以千位分隔符显示数字:
string make_commas(const uint64_t& num) {
// ... 函数实现 ...
}
main()函数生成线程并报告结果:
int main() {
vector<std::thread> swarm;
cout << format("spawn {} threads\n", max_threads);
for(int i{}; i < max_threads; ++i) {
swarm.emplace_back(countem, i);
}
ready = true;
for(auto& t : swarm) t.join();
cout << format("global count: {}\n", make_commas(g_count));
return 0;
}
在这里,我们创建了一个vector<std::thread>对象来保存线程。
在for循环中,我们使用emplace_back()在向量中创建每个线程。
一旦线程被生成,我们就设置ready标志,以便线程可以开始它们的循环。
输出:
spawn 100 threads
thread 67 won!
global count: 100,000,000
每次运行,获胜的线程都会不同。
它是如何工作的……
std::atomic类封装了一个对象,用于同步多个线程之间的访问。
封装的对象必须是一个平凡类型,这意味着它占用连续的内存,没有用户定义的构造函数,并且没有虚成员函数。所有基本类型都是平凡的。
虽然可以使用atomic与简单的struct一起使用:
struct Trivial {
int a;
int b;
};
std::atomic<Trivial> triv1;
虽然这种用法是可能的,但并不实用。除了设置和检索复合值之外,其他任何操作都会失去原子性的好处,并最终需要使用互斥锁。atomic类最适合标量值。
特殊化
atomic类有几个不同用途的特殊化:
指针和智能指针:std::atomic<U*>特殊化包括对原子指针算术操作的支持,包括fetch_add()用于加法和fetch_sub()用于减法。
浮点类型:当与浮点类型float、double和long double一起使用时,std::atomic包括对原子浮点算术操作的支持,包括fetch_add()用于加法和fetch_sub()用于减法。
整型类型:当与整型类型之一一起使用时,std::atomic提供对额外原子操作的支持,包括fetch_add()、fetch_sub()、fetch_and()、fetch_or()和fetch_xor()。
标准别名
STL为所有标准标量整型类型提供了类型别名。这意味着在我们的代码中,我们可以使用以下别名代替这些声明:
std::atomic_bool ready{};
std::atomic_uint64_t g_count{};
有46个标准别名,每个标准整型类型都有一个。
无锁变化
大多数现代架构提供了用于执行原子操作的原子CPU指令。如果硬件支持,std::atomic应该使用硬件支持的原子指令。一些原子类型可能在一些硬件上不受支持。对于这些特殊化,std::atomic可能会使用互斥锁来确保线程安全操作,导致线程在等待其他线程完成操作时阻塞。使用硬件支持的特殊化被称为无锁的,因为它们不需要互斥锁。
is_lock_free()方法检查一个特殊化是否是无锁的:
cout << format("is g_count lock-free? {}\n", g_count.is_lock_free());
输出:
is g_count lock-free? true
对于大多数现代架构,这个结果将是true。
有几种保证无锁的std::atomic变化可用。这些特殊化保证为每个目的使用最高效的硬件原子操作:
std::atomic_signed_lock_free是最高效的无锁有符号整型特殊化的别名。
std::atomic_unsigned_lock_free是最高效的无锁无符号整型特殊化的别名。
std::atomic_flag类提供了一个无锁原子布尔类型。
重要提示
当前的Windows系统即使在64位系统上也不支持64位硬件整数。当在我的实验室中的一个这样的系统上测试此代码时,将std::atomic<uint64_t>替换为std::atomic_unsigned_lock_free导致性能提高了3倍。在64位Linux和Mac系统上,性能没有变化。
还有更多……
当多个线程同时读写变量时,一个线程可能会以与它们被写入的顺序不同的顺序观察到这些更改。std::memory_order指定了围绕原子操作的内存访问顺序。
std::atomic提供了用于访问和更改其管理值的方法。与关联的操作符不同,这些访问方法提供了指定memory_order参数的选项。例如:
g_count.fetch_add(1, std::memory_order_seq_cst);
在这种情况下,memory_order_seq_cst指定了顺序一致性排序。因此,这个对fetch_add()的调用将以顺序一致性排序将1添加到g_count的值中。
可能的memory_order常量是:
memory_order_relaxed:这是一个放松的操作。不施加同步或排序约束;仅保证操作的原子性。
memory_order_consume:这是一个消耗操作。当前线程中依赖于该值的访问不能在此加载之前重新排序。这仅影响编译器优化。
memory_order_acquire:这是一个获取操作。访问不能在此加载之前重新排序。
memory_order_release:这是一个存储操作。当前线程中的访问不能在此存储之后重新排序。
memory_order_acq_rel:这既是获取又是释放。当前线程中的访问不能在此存储之前或之后重新排序。
memory_order_seq_cst:这是顺序一致性排序,根据上下文是获取或释放。加载执行获取,存储执行释放,读取/写入/修改执行两者。所有线程都以相同的顺序观察到所有修改。
如果没有指定memory_order,则默认值为memory_order_seq_cst。
- 上一篇: C++学习目标:最简单实用的布尔类型介绍
- 下一篇: 5分钟搞懂C++左值引用和右值引用
猜你喜欢
- 2024-12-06 面试经验:68个C/C++常见面试题汇总(含答案)
- 2024-12-06 C++猜数字游戏
- 2024-12-06 2023年9月 GESP C++ 一级真题及解析
- 2024-12-06 第十一届蓝桥杯青少组国赛C++试题真题
- 2024-12-06 博途中的 ANY指针
- 2024-12-06 c++基础知识汇总
- 2024-12-06 C++程序设计教程 面向对象程序设计
- 2024-12-06 C++引用10分钟入门教程
- 2024-12-06 C++反射之检测struct或class是否实现指定函数
- 2024-12-06 5分钟搞懂C++左值引用和右值引用
- 最近发表
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- apt-getinstall-y (100)
- node_modules怎么生成 (87)
- chromepost (71)
- flexdirection (73)
- c++int转char (80)
- mysqlany_value (79)
- static函数和普通函数 (84)
- el-date-picker开始日期早于结束日期 (76)
- js判断是否是json字符串 (75)
- c语言min函数头文件 (77)
- asynccallback (87)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 无效的列索引 (74)