优秀的编程知识分享平台

网站首页 > 技术文章 正文

C++ 使用std::atomic共享标志和值

nanyue 2024-12-06 18:02:52 技术文章 9 ℃


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。

Tags:

最近发表
标签列表