C++11 atomic及其内存序列和CAS

1.atomic及部分函数:

        atomic是c++11推出的原子变量,使用需要C++11及更高标准,包含的头文件为#include<atomic>

1.1 atomic变量:

        atomic可以声明基本类型的变量,如下:

std::atomic<int> a(0);
std::atomic<char> b('0');

        需要注意的是atomic变量不支持拷贝,因此我们不能让一个atomic变量等于另一个atomic变量。但是atomic可以使用=来将对应的基本类型赋值。下面是一些例子:        

std::atomic<int> a = 0;  //错误,禁用了拷贝构造函数,构造时无法使用=
std::atomic<int> a(0);  //正确
std::atomic<int> b = a;  //错误,禁用了拷贝构造函数
std::atomic<int> b(a);  //错误,同上
std::atomic<int> b(0); //正确
a=1;  //正确
b=a;  //错误,禁用了拷贝=

        atomic也支持自定义类型,但是并不支持所有的自定义类型。如果自定义类型在以下表达式的值均为true方可生成atomic变量:

auto ret = std::is_trivially_copyable<MY_UDT>::value;
ret = std::is_copy_constructible<MY_UDT>::value;
ret = std::is_move_constructible<MY_UDT>::value;
ret = std::is_copy_assignable<MY_UDT>::value;
ret = std::is_move_assignable<MY_UDT>::value;

         只有这五个表达式均为真才能生成该自定义结构的atomic变量。因此,自定义数据类型必须有拷贝赋值运算符、不能有任何虚函数或虚基类、必须使用编译器创建的拷贝赋值操作、该类型中所有的基类和非静态数据成员都需要支持拷贝赋值操作以及必须是“位可比的”。简单的说自定义类型如果可以使用memcpy来赋值并可以使用memcmp来按位比较。

        不过使用自定义类型如果需要对其原子化我们可以根据需要让它使用原子变量作成员变量。

1.2 is_lock_free()

        大多数原子变量是对数据进行无锁操作即可实现线程安全,但是针对一些变量是无法通过无锁操作来实现线程安全的,此时为保证变量可以实现原子操作,系统给它们内置了锁。而is_lock_free可以判断该变量是否是无锁的。大多数自定义数据类型都无法实现无锁保证原子性。

1.3 load和store

        首先原子类型和原类型不是同一个类型,因此我们需要使用原子类型作参数时应该如下写法:

int func1(int a);  //普通int
int func2(std::atomic<int> a);  //原子类型atomic<int>做参数
int func3(std::atomic<int>& a);  //引用传递

std::atomic<int> a;
func1(a);  //错误
func2(a);  //错误
func3(a);  //正确

       根据上面我们发现当原子类型作引用传递时可以正常运行,但是作形参时无法成功运行。原因很简单,前面我们已经说过atomic变量不支持拷贝,同样的也没有拷贝构造函数,而形参会根据参数构建临时对象,需要用到拷贝,因此原子类型无法作形参。实际上很多编译器会优化函数使部分函数不产生不必要的临时对象,但是从语法角度来讲是不可以这样的。

        我们可以通过load得到其对应的类型的实体,如下:

std::atomic<int> a(5);
std::cout<<a.load();  //输出5

        store可以设定原子变量实体的值,如下:

std::atomic<int> a(5);
a=6;  //此时a=6
a.store(7);  //此时a=7

        实际上原子类型重载的=用的也是store函数,源码如下:

__int_type operator=(__int_type __i) noexcept
{
	store(__i);
	return __i;
}

        但是store除了值之外还有一个参数,如下:

store(__int_type __i, memory_order __m = memory_order_seq_cst) noexcept

        我们发现还有第二个参数,而重载的=里面的store只有第一个参数,第二个用的是默认值。我们再看看load函数:

load(memory_order __m = memory_order_seq_cst) const noexcept

        我们发现它也有一个默认参数,而我们前面调用时并没有使用。实际上这是控制原子操作的内存顺序的选项,我们下一章将会讲解。而我们使用load和store函数而非=也与这个有关。

1.4 线程中使用atomic和不使用atomic对比:

        我们写一个简单的程序来观察结果:

#include <thread>
#include <atomic>
#include <unistd.h>
#include <iostream>

int func1(std::atomic<int>& a)
{
    for (int i = 0; i < 1000 * 100; i++) {
        a += 1;
    }
}

int func2(int& b)
{
    for (int i = 0; i < 1000 * 100; i++) {
        b += 1;
    }
}

int main()
{
    std::atomic<int> a(0);
    int b = 0;

    std::thread t1(func1, std::ref(a));
    std::thread t2(func1, std::ref(a));
    std::thread t3(func2, std::ref(b));
    std::thread t4(func2, std::ref(b));

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

    std::cout << a << ' ' << b << '\n';

    return 0;
}

        输出结果为:

        可以看出原子变量是线程安全的,而普通变量不是。

2.原子变量的内存顺序:

2.1 内存乱序:

        在我们编译程序时,程序执行顺序和代码顺序并不一定一致,先看下面程序:

int func(int a) {
    // 第一部分
    int b = a; // 1
    int c = b+1;  // 2
    int d = c+1;  // 3

    // 第二部分
    int x = a; // 4
    int y = x+1; // 5
    int z = y+1;  // 6
}

        代码顺序是123456,但是实际上并不一定是这个顺序。根据编译器优化等级可能第一部分和第二部分顺序会不一致。首先看第一部分,第2行b+1依赖于第1行的结果,第三行c+1依赖第二行的结果,因此这三行之间顺序不会变。同理,第二部分之中的456顺序不会变。但是我们发现第二部分中每行均不依赖与第一部分,所以每行都有可能出现在第一部分的任意位置之前。比如456123,124563,412536等,只要满足1,2,3之间顺序不变,4,5,6之间顺序不变均可能产生。

        上述内容对于单线程不会有影响,但是对于多线程可能会产生意外的结果,如下:

int a(0),b(0);

void func1()
{
    a=1;
    b=2;
}

void func2()
{
    std::cout<<a<<' '<<b<<'/n';
}

         如果我们不知道内存乱序,那么第二个结果可能有三种:

                a.func2在a=1之前运行完毕,输出{0,0};

                b.func2在a=1之后b=2之前执行完毕,输出{1,0};

                c.func2在b=2之后执行完毕,输出{1,2};

        显然如果顺序执行不会出现{0,2},因为顺序执行的话b赋值完成时a一定等于1,但是由于它是乱序执行,因此可能产生这种结果。

        多线程出现内存乱序主要原因是在于多核cpu执行指令时,当某一线程的一条指令执行完毕修改了值,该值可能存在某个CPU的缓存中,而其他CPU的缓存还是原来的值,这就导致读写不一致。

        对于避免乱序可以加互斥锁,不过互斥锁用在应对乱序上那么性能方面会大幅损耗,而原子变量提供的6种内存模型可以在性能开销相对较小的情况下保证顺序。

2.2 内存顺序概览:

        

        原子类型有6种内存操作选项,分别是:

        1.memory_order_relaxed

        2.memory_order_consume

        3.memory_order_acquire

        4.memory_order_release

        5.memory_order_acq_rel

        6.memory_order_seq_cst

        而我们在1.3中发现了load和store的默认参数是memory_order_seq_cst。所有的atomic类型内置的函数,如果可以选择操作选项却没有选择,那么默认便是memory_order_seq_cst。而内置的重载运算符如=,+,-由于未传递任何参数,因此它们都是以默认的memory_order_seq_cst方式调用相应函数。

        这六种内存操作选项代表三种内存模型,分别是:排序一致序列(memory_order_seq_cst),获取-释放序列(memory_order_consume, memory_order_acquire, memory_order_release和memory_order_acq_rel)和松散序列(memory_order_relaxed)。

2.3 排序一致序列和松散序列

        我们先来看看这两个相对极端的序列。排序一致性序列,即大多数方法默认的memory_order_seq_cst。排序一致性序列是对原子变量最强的约束,它会使一个多线程程序以某种特殊的排序执行,就像单线程一样。它是最安全的方式。不过安全带来的副作用是性能的损耗。松散序列即memory_order_relaxed,它只保证此次操作是原子的。看看下列代码:

std::atomic<bool> x(false),y(false);

void write_x_then_y()
{
  x.store(true,std::memory_order_relaxed);  // 1
  y.store(true,std::memory_order_relaxed);  // 2
}
void read_y_then_x()
{
  while(!y.load(std::memory_order_relaxed));  // 3
  assert(x.load(std::memory_order_relaxed));  // 4
}

        如果按顺序执行则4不会触发,但是由于使用的是std::memory_order_relaxed,那么有可能出现2->3->4->1的顺序,但是它仍时原子操作。我们看下面代码:

void func1()
{
    for (int i = 0; i < 1000 * 100; i++) {
        x.fetch_add(1, std::memory_order_relaxed);
    }
}

void func2()
{
    for (int i = 0; i < 1000 * 100; i++) {
        x.fetch_add(1, std::memory_order_relaxed);   
    }
}

int main()
{
    std::thread t1(func1);
    std::thread t2(func2);
    t1.join();
    t2.join();

    return 0;
}

        最后我们可以的到200000,这是正确值,因为即便是松散序列每个操作也是原子的。

2.4 获取释放序列:

        获取释放序列最常用的是这两个:memory_order_release和memory_order_acquire。memory_order_acquire在读取时可以加此参数,其之后的操作不能重排到该指令之前,memory_order_release在写入时可以加此参数,之前的操作不能排在该操作之后。执行memory_order_acquire类似互斥锁的lock,memory_order_release类似unlock,示例如下:

std::atomic<bool> x(false),y(false);

void write_x_then_y()
{
  x.store(true,std::memory_order_relaxed);  // 1
  y.store(true,std::memory_order_release);  // 2
}
void read_y_then_x()
{
  while(!y.load(std::memory_order_acquire));  // 3
  assert(x.load(std::memory_order_relaxed));  // 4
}

        此时执行第四步的断言时x一定为true。

        memory_order_consume和memory_order_acquire类似,但是它放宽了要求,其表示在代码中这条语句后面所有与这块内存有关的读写操作都无法被重排到这个操作之前,如下代码:

std::atomic<bool> x(false);
std::atomic<bool*> y;
y=new bool(false);

void write_x_then_y()
{
  x.store(true,std::memory_order_relaxed);  // 1
  y.store(true,std::memory_order_release);  // 2
}
void read_y_then_x()
{
  while(!y.load(std::memory_order_consume));  // 3
  assert(x.load(std::memory_order_relaxed));  // 4
}

        此时 1有可能被乱序到2后面,导致触发4断言。一般不建议使用这个,因为较高几率出错导致带来的代价一般要高于在此节省的效率成本。

        memory_order_acq_rel相当于memory_order_release和memory_order_acquire的结合,同时约束该语句前面语句和后面语句的顺序。

2.5 对非原子的操作排序

        对非原子操作和原子操作混用时,可以将非原子操作近似看成memory_order_relaxed,因此当含有顺序一致性序列和获取释放队列时也会隔离非原子操作。

3.CAS

3.1 CAS简介:

        CAS是一种常见的无锁算法,其流程简单概括如下:

do { 
     备份旧数据;
     构造新数据;
}while(!CAS(内存地址,旧数据,新数据))

        其中CAS将比较旧数据和内存数据是否相同,如果相同,我们认为这个值没有被改变,所以可以写入新值,反之则说明在构造新数据时已经有其它线程把其它新值写入内存地址,此时则重新进行备份和构造操作。

        上述操作仅判断了是否相对,但是引入了ABA问题即另一个线程可能会把变量的值从A改成B,又从B改回成A。很多情况下,ABA问题不会影响程序逻辑,此时可以忽略。但有时不能忽略。一般的做法是给变量关联一个只能递增、不能递减的版本号,在CAS比较时再比较版本号。

3.2 atomic的CAS函数:

        atomic提供了两个CAS函数,compare_exchange_strong 与 compare_exchange_weak。使用方法如下:

void func1()
{
    for (int i = 0; i < 100 * 1000; i++) {
        int x1 = x;
        while (!x.compare_exchange_weak(x1, x + 1));
    }
}

void func2()
{
    for (int i = 0; i < 100 * 1000; i++) {
        int x1 = x;
        while (!x.compare_exchange_strong(x1, x + 1));
    }
}

        其中weak里x的内存和x1相等时有可能返回false,但是效率比strong快的多。此外使用循环可以避免weak产生的问题。在x86平台weak不会发生此问题。

3.3 使用CAS实现自旋锁:

        实现简单的自旋锁代码如下:

int a(0);

class SpinLock {
public:
    SpinLock() : flag(false)
    {}

    void lock()
    {
        bool expect = false;
        while (!flag.compare_exchange_weak(expect, true, std::memory_order_relaxed))
        {
            expect = false;
        }
    }

    void unlock()
    {
        flag.store(false, std::memory_order_relaxed);
    }

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

SpinLock spinLock;

void func1()
{
    for (int i = 0; i < 1000 * 1000 * 100; i++) {
        spinLock.lock();
        a++;
        spinLock.unlock();
    }
}

void func2()
{
    for (int i = 0; i < 1000 * 1000 * 100; i++) {
        spinLock.lock();
        a++;
        spinLock.unlock();
    }
}

int main()
{
    std::thread t1(func1);
    std::thread t2(func2);

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

    std::cout<<a<<"\n";

    return 0;
}

        最后可以得到正确的结果。下面实现一个类似lock_guard的:

int a(0);

class SpinLock {
public:
    SpinLock() : flag(false)
    {}

    void lock()
    {
        bool expect = false;
        while (!flag.compare_exchange_weak(expect, true, std::memory_order_relaxed))
        {
            expect = false;
        }
    }

    void unlock()
    {
        flag.store(false, std::memory_order_relaxed);
    }

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

SpinLock spinLock;

class SpinLockGuard {
public:
    SpinLockGuard(SpinLock& spinlock) : spinLock(spinlock) {
        spinLock.lock();
    }

    ~SpinLockGuard() {
        spinLock.unlock();
    }

protected:
    SpinLock& spinLock;
};

void func1()
{
    for (int i = 0; i < 1000 * 1000 * 10; i++) {
        SpinLockGuard lck(spinLock);
        a++;
    }
}

void func2()
{
    for (int i = 0; i < 1000 * 1000 * 10; i++) {
        SpinLockGuard lck(spinLock);
        a++;
    }
}

int main()
{
    std::thread t1(func1);
    std::thread t2(func2);

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

    std::cout<<a<<"\n";

    return 0;
}

        这样我们就可以实现简单的自旋锁了。 

  • 21
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言中的`<stdatomic.h>`库提供了原子操作接口,用于实现线程安全的原子操作。原子操作是不可被中断的,即在执行原子操作期间不会有其他线程干扰。 在C11标准中,`<stdatomic.h>`库引入了一组原子类型和原子操作函数。这些原子类型和函数可以用于实现共享变量的原子访问、更新和同步。 下面是一些常用的原子类型和相关函数: 1. `atomic_flag`类型:用于简单的原子标志操作,只有两个操作:`atomic_flag_test_and_set()`和`atomic_flag_clear()`。 2. 原子整型类型(如`atomic_int`、`atomic_uint`等):支持常见的整型操作,如赋值、加法、减法、比较交换等。 3. 原子指针类型(如`atomic_intptr_t`、`atomic_ptrdiff_t`等):支持指针类型的原子操作,如原子加载、存储和比较交换等。 4. `atomic_thread_fence()`函数:用于实现内存屏障,确保指令重排序不会破坏多线程程序的正确性。 5. `atomic_load()`和`atomic_store()`函数:用于原子加载和存储操作。 6. `atomic_exchange()`函数:用于原子交换操作,可以原子地交换一个值并返回旧值。 7. `atomic_compare_exchange_strong()`和`atomic_compare_exchange_weak()`函数:用于原子比较并交换操作,可以原子地比较并交换一个值。 通过使用这些原子类型和函数,我们可以实现线程安全的并发操作。注意,原子操作并不意味着完全的线程同步,额外的同步机制(如互斥锁)可能仍然是必需的来确保正确的并发访问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值