草庐IT

【Example】C++ 标准库 std::atomic 及 std::memory_order

芯片烤电池的草稿箱 2023-03-28 原文

C++ 标准库提供了原子操作。(我已经懒得写序言了)

 

====================================

先来说原子操作的概念:

原子操作是多线程当中对资源进行保护的一种手段,主要作用是和互斥量(Mutex)一样,避免对资源的并发访问、修改。

互斥量的粒度衡量是作用域(哪怕作用域内只有一个变量),而原子的粒度衡量则是以一个变量或对象为单位。因此,原子相对于互斥量更加高效,但并非替代关系。

互斥量的主要作用是保护作用域内的资源,而原子的作用是保护一个变量或对象。

因此,当你需要保护的资源仅仅是某个变量或对象时,应首先考虑使用原子。

 

1,std::atomic

头文件:

#include <atomic>

 

std::atomic 是一个模板类,它的语法是:

std::atomic<Type> name(default_value);

 

如果你并不明白 std::atomic (原子) 的作用,请看以下代码及执行结果:

#include <iostream>
#include <atomic>
#include <thread>

int basic_value(0);
void ThreadChangeValue() {
    for (size_t i = 0; i < 1000000; i++)
    {
        basic_value++;
    }
    return;
}

std::atomic<int> atomic_int_value(0);
void ThreadChangeAtomic() {
    for (size_t i = 0; i < 1000000; i++)
    {
        atomic_int_value.fetch_add(1, std::memory_order_relaxed);
    }
    return;
};

int main()
{
    std::thread t1(ThreadChangeValue);
    std::thread t2(ThreadChangeValue);

    std::thread t3(ThreadChangeAtomic);
    std::thread t4(ThreadChangeAtomic);

    t1.join();
    t2.join();

    t3.join();
    t4.join();

    std::cout << "Basic Value: " << basic_value << std::endl;
    std::cout << "Atomic Value: " << atomic_int_value << std::endl;

    return EXIT_SUCCESS;
}

 

执行结果:

Basic Value: 1123299
Atomic Value: 2000000

 

以上代码分别定义了两个 int 变量,一个是普通的变量,一个是原子变量。两个变量分别用两个线程去递增1000000次。

理论上,两个变量最终值应同为2000000。然而,普通变量却出现了资源竞争性错误,两个线程都有接近一半的操作都是失败的,导致最终值仅为1123299。

而受原子保护的变量,两个线程的操作则全部成功。

 

std::atomic 的成员函数表:

名称 作用 适用内存序
operator= 重载等  
operator T 从原子对象加载值  
store 用另一个非原子值替换当前原子化的值 对象类型必须和原子对象声明时一致

memory_order_relaxed

memory_order_release

memory_order_seq_cst

load 从原子对象当中加载值(返回)

memory_order_relaxed

memory_order_consume

memory_order_acquire

memory_order_seq_cst

is_lock_free 检查原子对象的锁定状态  
wait 【std20】 阻塞线程至被提醒且原子值更改  
exchange 用另一个原子值替换当前原子值 并返回先前的原子值

memory_order_relaxed

memory_order_consume

memory_order_acquire

memory_order_release

memory_order_acq_rel

memory_order_seq_cst

compare_exchange_weak

原子地比较原子对象与非原子参数的值,若相等则进行交换,若不相等则进行加载

(允许少部分不符合条件的值返回)

memory_order_relaxed

memory_order_consume

memory_order_acquire

memory_order_release

memory_order_acq_rel

memory_order_seq_cst

compare_exchange_strong 原子地比较原子对象与非原子参数的值,若相等则进行交换,若不相等则进行加载  

memory_order_relaxed

memory_order_consume

memory_order_acquire

memory_order_release

memory_order_acq_rel

memory_order_seq_cst

notify_one【std20】 通知至少一个在该原子对象等待线程  
notify_all【std20】
通知所有在该原子对象等待线程  
[常量] is_always_lock_free
指示该类型是否始终免锁  

 

除此之外 std::atomic 还对 int 及指针类型做了特殊化增强,以下操作函数仅适用于 int 及指针类型操作:

额外备注:C++ 20 后部分特化支持 float 。

 

名称 作用 适用特化类型 适用内存序
fetch_add 原子地将参数加到存储于原子对象的值,并返回先前保有的值 int && ptr && float(std20)

memory_order_relaxed

memory_order_consume

memory_order_acquire

memory_order_release

memory_order_acq_rel

memory_order_seq_cst

fetch_sub 原子地从存储于原子对象的值减去参数,并获得先前保有的值 int && ptr && float(std20)

memory_order_relaxed

memory_order_consume

memory_order_acquire

memory_order_release

memory_order_acq_rel

memory_order_seq_cst

fetch_and 原子地进行参数和原子对象的值的逐位与,并获得先前保有的值 int

memory_order_relaxed

memory_order_consume

memory_order_acquire

memory_order_release

memory_order_acq_rel

memory_order_seq_cst

fetch_or 原子地进行参数和原子对象的值的逐位或,并获得先前保有的值 int

memory_order_relaxed

memory_order_consume

memory_order_acquire

memory_order_release

memory_order_acq_rel

memory_order_seq_cst

fetch_xor 原子地进行参数和原子对象的值的逐位异或,并获得先前保有的值 int

memory_order_relaxed

memory_order_consume

memory_order_acquire

memory_order_release

memory_order_acq_rel

memory_order_seq_cst

operator++ 原子值递增 int && ptr  
operator-- 原子值递减 int && ptr  
operator+= 原子值增加 int && ptr && float(std20)  
operator-= 原子值减少 int && ptr && float(std20)  
operator&= 进行原子按位与 int  
operator|= 进行原子按位或 int  
operator^= 进行原子按位异或 int  

 

 

额外补充 std::atomic_flag :

 std::atomic_flag 是原子的最基本布尔类型,它是无锁的,并且它没有拷贝构造函数,也不提供 load 和 store 操作。主要用于提供比 std::atomic 更简单基本化布尔操作效率。

 

构造语法:

std::atomic<bool> name(false);
std::atomic_flag name = ATOMIC_FLAG_INIT;

 

成员函数表:

名称 作用
operator= 重载等
clear 将布尔值设置为 false
test_and_set 将布尔值设置为 true 并返回先前值
test【std20】 原子的返回当前值
wait 阻塞线程至被提醒且原子值更改
notify_one【std20】 通知至少一个在该原子对象等待线程
notify_all【std20】 通知所有在该原子对象等待线程

 

 2,std::memory_order

std::memory_order 指定内存访问,包括常规的非原子内存访问,如何围绕原子操作排序。在没有任何制约的多处理器系统上,多个线程同时读或写数个变量时,一个线程能观测到变量值更改的顺序不同于另一个线程写它们的顺序。其实,更改的顺序甚至能在多个读取线程间相异。一些类似的效果还能在单处理器系统上出现,因为内存模型允许编译器变换。

库中所有原子操作的默认行为提供序列一致顺序(见后述讨论)。该默认行为可能有损性能,不过可以给予库的原子操作额外的 std::memory_order 参数,以指定附加制约,在原子性外,编译器和处理器还必须强制该操作。

-- 《C++ Reference》

 

要理解内存序是做什么的,要先从硬件讲起:(尽量简单通俗)

以一颗 CPU i7-10875H 为例,它有8颗物理内核,从物理上来讲,它可以同时处理8条并行线程,通过超线程技术可以扩展到16条线程(物理上还是8条)。

再在软件层面来讲,并行的数千条线程是逻辑并行,终究都要交给 CPU 进行串行处理,而 CPU 可以同时处理的线程数量,就是由内核数量决定的。

而每个 CPU 内核所运算数据的存取,并不是直接存取到内存当中,而是要先经过每个内核互相独立的 L1、L2 两级高速缓存,再到 CPU 内核之间共享的 L3 高速缓存,再然后到内存。

 

这样就造成了一个问题,就是,假设一个内核负责的一条线程修改了某个变量的值,但是还没有刷新到内核之间共享的 L3 缓存或者内存之中,那么这时候其他 CPU 内核从内存中读取到的该变量就仍然是旧值。

所以,为了避免这种情况,这就是 std::memory_order 的作用。

 

首先,要明白 std::memory_order 本身是什么,它是定义于 <atomic> 头文件当中的六个枚举值,使用时用做参数传递给 std::atomic 相关的操作函数,比如 load、store 等。

支持传 std::memory_order 枚举的相关操作函数上文都已经列出,这里重点将这六个枚举都代表什么。

 

std::memory_order 枚举值说明:

名称 作用
memory_order_relexed 只保证原子值不被其他线程同时访问,但没有线程之间同步、顺序制约,其他线程可能读取到内存当中的旧值。
memory_order_consume

[C++17注:目前不建议使用]有顺序的加载操作,只影响到当前线程。

作用是保证之后的load操作不会排在声明该枚举值的当前load操作之前。

memory_order_acquire 有顺序的加载操作,作用是保证之后所有线程的load操作不会排在声明该枚举值的当前load操作之前。
memory_order_release 有顺序的释放操作,作用是保证之后的 load(读)、store(写) 性质操作不会排在传入该枚举值的操作函数之前。
memory_order_acq_rel

有顺序整合加载(memory_order_acquire)->释放(memory_order_release)操作。

当前线程的所有 load(读)、store(写) 性质操作不会排在传入该枚举值的操作函数之前后。

所有带有释放(memory_order_release)操作同一原子对象的线程会排在传入该枚举值的操作函数之前。

而且当前线程对原子值的修改会同步给其他进行读操作的同一原子对象的线程。

memory_order_seq_cst

传入该枚举值的操作函数,load(读) 时会进行 memory_order_acquire 操作,store(写)时会进行 memory_order_release 操作。

如果是读+写就是 memory_order_acq_rel 操作。

备注:此枚举值为支持传入 std::memory_order 操作函数的缺省值。

 

 

以下代码演示了一些最简单的使用:

 

下例演示两个线程间传递性的释放获得顺序:

#include <iostream>
#include <atomic>
#include <thread>
#include <string>

std::atomic<std::string*> atom_str(nullptr);
int flag = 0;
void Producer()
{
    std::string* str = new std::string("Hello Byte");
    flag = 1;
    atom_str.store(str, std::memory_order_release);
    return;
}

void Consumer()
{
    std::string* str;
    while (!(str = atom_str.load(std::memory_order_acquire)));
    
    if (flag != 1)
    {
        // 绝不会执行
        std::cout << "Error..." << std::endl;
    }
    else
    {
        std::cout << str->c_str() << std::endl;
    }

    return;
}


int main()
{
    std::thread t1(Producer);
    std::thread t2(Consumer);

    t1.join();
    t2.join();

    if (atom_str.load() != nullptr)
    {
        delete atom_str.load();
    }

    return EXIT_SUCCESS;
}

 

 

 

下例演示三个线程间传递性的释放获得顺序:

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::vector<int> data;
std::atomic<int> flag(0);
void Producer()
{
    data.push_back(42);
    flag.store(1, std::memory_order_release);
    return;
}

void Broker() {
    int expected = 1;
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) 
    {
        expected = 1;
    }
};

void Consumer()
{
    while (flag.load(std::memory_order_acquire) < 2);

    if (data[0] != 42)
    {
        // 绝不会执行
        std::cout << "False..." << std::endl;
    }
    else
    {
        std::cout << "True..." << std::endl;
    }

    return;
}

int main()
{
    std::thread t1(Producer);
    std::thread t2(Broker);
    std::thread t3(Consumer);

    t1.join();
    t2.join();
    t3.join();

    return EXIT_SUCCESS;
}

 

有关【Example】C++ 标准库 std::atomic 及 std::memory_order的更多相关文章

  1. ruby - 将 spawn() 的标准输出/标准错误重定向到 Ruby 中的字符串 - 2

    我想使用spawn(针对多个并发子进程)在Ruby中执行一个外部进程,并将标准输出或标准错误收集到一个字符串中,其方式类似于使用Python的子进程Popen.communicate()可以完成的操作。我尝试将:out/:err重定向到一个新的StringIO对象,但这会生成一个ArgumentError,并且临时重新定义$stdxxx会混淆子进程的输出。 最佳答案 如果你不喜欢popen,这是我的方法:r,w=IO.pipepid=Process.spawn(command,:out=>w,:err=>[:child,:out])

  2. ruby-on-rails - 标准化文件名的字符串,删除重音和特殊字符 - 2

    我正在尝试找到一种方法来规范化字符串以将其作为文件名传递。到目前为止我有这个:my_string.mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/n,'').downcase.gsub(/[^a-z]/,'_')但第一个问题:-字符。我猜这个方法还有更多问题。我不控制名称,名称字符串可以有重音符、空格和特殊字符。我想删除所有这些,用相应的字母('é'=>'e')替换重音符号,并将其余的替换为'_'字符。名字是这样的:“Prélèvements-常规”“健康证”...我希望它们像一个没有空格/特殊字符的文件名:“prelevements_routin

  3. ruby-on-rails - rails group by 和 order by column - 2

    在我的Controller中,我得到了按类别分组的所有Extras:defindex@categories=Extra.all.group_by(&:category)end结果类似于哈希数组:{#=>[#,#=>[#,#]}我想按类别“排序”列而不是id排序,它应该如下所示:{#=>[#,#=>[#,#]}当我尝试时:defindex@categories=Extra.all.group_by(&:category).sort_by{|s|s[:sort]}end我得到“没有将符号隐式转换为整数”。那是因为我在“sort_by”中使用了一个符号吗? 最佳答

  4. Ruby:标准递归模式 - 2

    我经常迷上ruby​​的一件事是递归模式。例如,假设我有一个数组,它可能包含无限深度的数组作为元素。所以,例如:my_array=[1,[2,3,[4,5,[6,7]]]]我想创建一个方法,可以将数组展平为[1,2,3,4,5,6,7]。我知道.flatten可以完成这项工作,但这个问题是作为我经常遇到的递归问题的一个例子-因此我试图找到一个更可重用的解决方案。简而言之-我猜这种事情有一个标准模式,但我想不出任何特别优雅的东西。任何想法表示赞赏 最佳答案 递归是一种方法,它不依赖于语言。您在编写算法时要考虑两种情况:再次调用函数的情

  5. ruby-on-rails - 使用 Ruby 标准 Logger 每天只创建一个日志 - 2

    我正在使用ruby​​标准记录器,我想要每天轮换一次,所以在我的代码中我有:Logger.new("#{$ROOT_PATH}/log/errors.log",'daily')它运行完美,但它创建了两个文件errors.log.20130217和errors.log.20130217.1。如何强制它每天只创建一个文件? 最佳答案 您的代码对于长时间运行的应用程序是正确的。发生的事情是您在给定的一天多次运行代码。第一次运行时,Ruby会创建一个日志文件“errors.log”。当日期改变时,Ruby将文件重命名为“errors.log

  6. ruby-on-rails - 在 Ruby 或 Rails 中,hash.merge({ :order => 'asc' }) can return a new hash with a new key. 什么可以返回带有已删除键的新散列? - 2

    在Ruby(或Rails)中,我们可以做到new_params=params.merge({:order=>'asc'})现在new_params是一个带有添加键:order的散列。但是是否有一行可以返回带有已删除key的散列?线路new_params=params.delete(:order)不会工作,因为delete方法返回值,仅此而已。我们必须分3步完成吗?tmp_params=paramstmp_params.delete(:order)returntmp_params有没有更好的方法?因为我想做一个new_params=(params[:order].blank?||para

  7. 用于从 Open3.popen3 标准输出中提取值的正则表达式 - 2

    如何获取外部命令的输出并从中提取值?我有这样的东西:stdin,stdout,stderr,wait_thr=Open3.popen3("#{path}/foobar",configfile)if/exit0/=~wait_thr.value.to_srunlog.puts("Foobarexitednormally.\n")puts"Testcompleted."someoutputvalue=stdout.read("TX.*\s+(\d+)\s+")puts"Outputvalue:"+someoutputvalueend我没有在标准输出上使用正确的方法,因为Ruby告诉我它不能

  8. ruby - 强制 Ruby 不以标准形式/科学记数法/指数记数法输出 float - 2

    我遇到了同样的问题here对于python,但对于ruby​​。我需要输出这样一个小数字:0.00001,而不是1e-5。有关我的特定问题的更多信息,我正在使用f.write("Mynumber:"+small_number.to_s+"\n")输出到一个文件对于我的问题,准确性不是什么大问题,所以只做一个if语句来检查是否small_number那么更通用的方法是什么? 最佳答案 f.printf"Mynumber:%.5f\n",small_number您可以将.5(小数点右侧5位数字)替换为您喜欢的任何特定格式大小,例如,%8

  9. ruby-on-rails - Order Hash 并删除第一个键值对 - 2

    我有一个以时间戳为键的哈希。hash={"2016-05-31T22:30:58+02:00"=>{"path"=>"/","method"=>"GET"},"2016-05-31T22:31:23+02:00"=>{"path"=>"/tour","method"=>"GET"},"2016-05-31T22:31:05+02:00"=>{"path"=>"/contact_us","method"=>"GET"}}我订购了这个系列并得到了第一双这样的:hash.sort_by{|k,_|k}.first.first但是我该如何删除它呢?删除方法requiresyou知道key的准确

  10. ruby - 在 Heroku Cedar 上的 Rails 3.2 中,是否有一种标准的方式来提供预压缩的 Assets ? - 2

    我有一个正在HerokuCedar堆栈上部署的Rails3.2应用程序。这意味着应用程序本身负责为其静态Assets提供服务。我希望对这些Assets进行gzip压缩,所以我在production.rb的中间件堆栈中插入了Rack::Deflater:middleware.insert_after('Rack::Cache',Rack::Deflater)...curl告诉我这与宣传的一样有效。但是,由于Heroku将全力运行rakeassets:precompile,生成一堆预gzipAssets,我很想使用它们(而不是让Rack::Deflater再次完成所有工作)。我已经看到使用

随机推荐