C++多线程编程(四): atomic原子操作

0 前言

原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。在编程语言中,有些操作虽然看起来只有一行,但是变成机器语言后就是多个操作步骤,其中的每个操作步骤都是一个原子操作,但是这些操作合起来却不是原子操作,这样的代码在并发执行时可能会调度到其他线程,从而出现中断的情况,造成数据不一致。

1 非原子操作存在的问题

#include <iostream>
#include <thread>

int cnt = 0;
 
void func() {
    int c = 10000;
    while(c--) {
        ++cnt;
    }
}
 
int main(){
    std::thread th1(func);
    std::thread th2(func);

    th1.join();
    th2.join();
    std::cout << cnt << std::endl;

    return 0;
}

两个线程同时对共享变量cnt执行1万次自增操作,然后在主函数中打印共享变量cnt的值,如果多次执行该代码,会发现最终的结果可能不是2万。虽然++cnt看起来只有一行代码,但是在翻译为机器代码时会分成多个步骤:先将cnt放到eax寄存器,然后对eax寄存器加1,最后再将eax寄存器结果放回到cnt。如果多个线程同时执行,这三个操作可能会被打断造成最终的结果错误。

2 原子操作的基本使用

为了解决并发执行的非原子操作造成的数据不一致问题,一种方式是可以加锁,例如用std::mutex对上面的++cnt加锁。另一种方式是使用C++11引入的atomic原子类型,原子类型提供的各种操作都是原子的。

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

std::atomic<int> cnt(0);
 
void func() {
    int c = 10000;
    while(c--) {
        ++cnt;
    }
}
 
int main(){
    std::thread th1(func);
    std::thread th2(func);

    th1.join();
    th2.join();
    std::cout << cnt << std::endl;

    return 0;
}

上述代码与之前的代码只有两个不同点:

  • 引入atomic头文件
  • 将基本的int类型换成std::atomic类型,并使用0进行初始化

多次运行会发现输出的结果一定是2万。

std::atomic本身是个模板类型,内部保存了基本数据类型的数据,原子对象保证所有的方法在操作数据时不被中断。

3 原子操作的方法

std::atomic提供了两大类操作,一类是对所有原子类型的通用方法,另一类是只对特定的原子类型适用的方法。

通用方法:

  • store:更新原子对象的数据
  • load:获取原子对象的数据
  • is_lock_free:是否支持原子操作
  • exchange:将原子对象的数据与另一个数据进行交换,返回值是交换前原子对象的数据
  • compare_exchange_weak:传入期望值和设定值,将原子对象的数据与期望值进行比较,如果相等,则将原子对象的数据赋值为设定值,如果不相等,则将原子对象的数据更新到期望值
  • compare_exchange_strong:与compare_exchange_weak类似,并且可以防止compare_exchange_weak在意外情况下返回false的情况

特定方法:

  • fetch_add/fetch_sub:对原子对象的数据进行加和减(整数)
  • fetch_and/fetch_or/fetch_xor:位运算(整数)
  • operator++/operator–:自增和自减(整数)
  • 复合赋值运算:+=、-=(整数和指针);&=、|=、^=(整数)

总的来说就是:所有的原子对象都支持对内部保存的数据的读取和更新;对于整数的原子对象,支持与常规整数相同的复合赋值运算以及自增和自减。

上述操作里面大部分都是数据读写和对整数的常规操作,比较特别的只有is_lock_free和compare_exchange_weak/compare_exchange_strong。

is_lock_free用于判断当前平台是否可以使用原子操作,因为std::atomic的实现是基于底层硬件的,只有当底层硬件实现了操作的原子性,才能使用std::atomic。但是当前大部分环境都支持原子操作,因此,通常不需要执行该函数。

compare_exchange_weak/compare_exchange_strong则用于实现CAS:CAS(Compare And Swap)是一种更新变量的机制,常用于实现无锁数据结构和算法。具体使用场景为:首先获取当前字段值,然后进行操作,在需要更新时调用compare_exchange,compare_exchange会检查当前值与之前得到的值是否相同,如果相同,说明这段时间(获取当前字段值和更新字段值这段时间)内没有其他线程修改,可以安全的进行更新,否则,就只能重新获取当前的值再次更新。kubernetes里面有类似的机制:每个资源对象的metadata都有一个resourceVersion属性,每次资源更新时,该属性就会自增,当客户端需要更新资源对象,在提交yaml到服务端时,服务端会检查客户端提交的资源更新的resourceVersion,如果跟服务端保存的相同,说明资源没有被更新过,本次可以安全的更新,如果跟服务端保存的不同,说明从上次获取到本地变更之间资源已经被修改了,不能被更新。通常情况下建议使用compare_exchange_strong,虽然它的性能比compare_exchange_weak稍低,但是它提供了更强的数据一致性保证。

4 atomic_flag

除了std::atomic,atomic中还提供了另一种简单的原子布尔类型:atomic_flag,而且它只提供两个方法:test_and_set、clear。

atomic_flag是原子布尔类型,那么可以理解为,atomic_flag只有两个状态:true和false。

  • test_and_set:检查标志位,如果标志位为true,则直接返回true,如果标志位为false,则更新为true,并返回false
  • clear:清除标志位,将标志位设置为false

它们的逻辑大概如下:

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

class my_atomic_flag {
    public:
        my_atomic_flag() {
            val = false;
        }

        bool test_and_set() {
            if(val == false) {
                val = true;
                return false;
            } else {
                return true;
            }
        }

        void clear() {
            val = false;
        }

    private:
        std::atomic<bool> val;
};

my_atomic_flag flag;
int cnt;

void func() {
    while(flag.test_and_set()) {}

    int c = 10000;
    while(c--) {
        ++cnt;
    }

    flag.clear();
}
 
int main(){
    std::thread th1(func);
    std::thread th2(func);

    th1.join();
    th2.join();
    std::cout << cnt << std::endl;

    return 0;
}

当test_and_set返回true时,说明标志位已经被设置过,可以理解为锁已经被占用,此时可以等待锁;如果test_and_set返回false时,说明当前线程占用锁,然后就可以执行对应的逻辑,在执行逻辑结束后执行clear清除标志。这种使用方法是典型的自旋锁。

使用atomic_flag还可以实现简单的条件变量,使用test_and_set检查条件是否满足。

5 总结

  • 原子操作是不能被分割和中断的操作,在多线程并发环境中修改共享数据时,由于数据更新操作不是原子的,会造成数据不一致
  • 可以通过加锁操作实现数据的互斥修改,防止数据不一致,但是加锁会降低性能
  • C++11提供了std::atomic,它基于底层硬件提供的原子操作能力,为了判断当前环境是否支持原子操作,std::atomic提供了is_lock_free()
  • std::atomic对常见的基本数据类型进行包装,并提供了原子级别的数据读取、更新和CAS操作;对于整数类型的原子对象,还支持算术和逻辑运算。std::atomic的主要使用场景就是数据的原子更新和无锁编程
  • std::atomic_flag是原子布尔类型,只提供了test_and_set和clear操作,可以实现类似自旋锁、条件变量等能力
  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
std::atomic_bool是C++14中的原子布尔类型。它提供了原子操作,以确保多线程环境下的安全性。它可以用于实现锁、同步和并发算法等。使用std::atomic_bool可以保证对布尔类型的读写操作在多线程环境中是原子的,即不会发生竞态条件。 在C++14中,std::atomic_bool比std::atomic_flag功能更全,可以使用非原子的bool来赋值和初始化。例如,你可以这样使用std::atomic_bool: std::atomic_bool b(true); b = false; 这样就可以对b进行原子的赋值操作。 需要注意的是,std::atomic_flag由于限制性甚至不能用作一个通用的布尔标识,所以最好还是使用std::atomic_bool。在C语言中,也可以使用自旋锁来实现多线程的同步。以下是一个使用自旋锁实现的例子: #include <thread> #include <vector> #include <iostream> #include <atomic> std::atomic_flag lock = ATOMIC_FLAG_INIT; void f(int n) { for (int cnt = 0; cnt < 5; cnt++) { while (lock.test_and_set(std::memory_order_acquire)) ; // 自旋 std::cout << "Thread " << n << " count:" << cnt << std::endl; lock.clear(std::memory_order_release); // 释放锁 } } int main(int argc, char* argv[]) { std::vector<std::thread> v; for (int n = 0; n < 4; n++) { v.emplace_back(f, n); //使用参数进行初始化 } for (auto& t : v) { t.join(); //等待线程结束 } system("pause"); return 0; } 这个例子中,使用std::atomic_flag作为锁,通过test_and_set()和clear()函数来获得锁和释放锁。 总结来说,std::atomic_bool是C++14中的原子布尔类型,可以用于多线程环境下的安全操作。在C语言中,可以使用自旋锁来实现多线程的同步。<span class="em">1</span><span class="em">2</span><span class="em">3</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值