C++11 原子操作(atomic operation)

所谓的原子操作,取的就是“原子是最小的、不可分割的最小个体”的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。

在以往的C++标准中并没有对原子操作进行规定,我们往往是使用汇编语言,或者是借助第三方的线程库,例如intel的pthread来实现。在新标准C++11,引入了原子操作的概念,并通过这个新的头文件提供了多种原子操作数据类型,例如,atomic_bool,atomic_int等等,如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证,多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。

我们还是来看一个实际的例子。假若我们要设计一个广告点击统计程序,在服务器程序中,使用多个线程模拟多个用户对广告的点击:

#include <boost/thread/thread.hpp>
#include <atomic> 
#include <iostream>
#include <time.h>

using namespace std;
// 全局的结果数据 
long total = 0; 

// 点击函数
void click()
{
    for(int i=0; i<1000000;++i)
    {
        // 对全局数据进行无锁访问 
        total ++;     
    }
}
 
 
int main(int argc, char* argv[])
{
    // 计时开始
    clock_t start = clock();
    // 创建100个线程模拟点击统计
    boost::thread_group threads;
    for(int i=0; i<100;++i) 
    {
        threads.create_thread(click);
    }

    threads.join_all();
    // 计时结束
    clock_t finish = clock();
    // 输出结果
    cout<<"result:"<<total<<endl;
    cout<<"duration:"<<finish -start<<"ms"<<endl;
    return 0;
}

 

从执行的结果来看,这样的方法虽然非常快,但是结果不正确

E:\SourceCode\MinGW>thread.exe
result:87228026
duration:528ms

 


很自然地,我们会想到使用互斥对象来对全局共享资源的访问进行保护,于是有了下面的实现:

long total = 0;
// 对共享资源进行保护的互斥对象
mutex m;

void click()
{
    for(int i=0; i<1000000;++i)
    {
        // 访问之前,锁定互斥对象
        m.lock();
        total++;
        // 访问完成后,释放互斥对象 
        m.unlock();
    }
}

 


互斥对象的使用,保证了同一时刻只有唯一的一个线程对这个共享进行访问,从执行的结果来看,互斥对象保证了结果的正确性,但是也有非常大的性能损失,从刚才的528ms变成了现在的8431,用了原来时间的10多倍的时间。这个损失够大。

E:\SourceCode\MinGW>thread.exe
result:100000000
duration:8431ms

 

如果是在C++11之前,我们的解决方案也就到此为止了,但是,C++对性能的追求是永无止境的,他总是想尽一切办法榨干CPU的性能。在C++11中,实现了原子操作的数据类型(atomic_bool,atomic_int,atomic_long等等),对于这些原子数据类型的共享资源的访问,无需借助mutex等锁机制,也能够实现对共享资源的正确访问。

// 引入原子数据类型的头文件
#include <atomic> 
 

// 用原子数据类型作为共享资源的数据类型
atomic_long total(0);
//long total = 0;
 
void click()
{
    for(int i=0; i<1000000;++i)
    {
        // 仅仅是数据类型的不同而以,对其的访问形式与普通数据类型的资源并无区别
        total ++;
    }
}

 

我们来看看使用原子数据类型之后的效果如何:

E:\SourceCode\MinGW>thread.exe
result:100000000
duration:2105ms

 

结果正确!耗时只是使用mutex互斥对象的四分之一!也仅仅是不采用任何保护机制的时间的4倍。可以说这是一个非常不错的成绩了。

原子操作的实现跟普通数据类型类似,但是它能够在保证结果正确的前提下,提供比mutex等锁机制更好的性能,如果我们要访问的共享资源可以用原子数据类型表示,那么在多线程程序中使用这种新的等价数据类型,是一个不错的选择。

 

六种原子操作内存顺序

知乎的一位答主讲的比较不错,点此,再加之深究《C++并发编程实战》第五章的内容。

//这里枚举这六种
typedef enum 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 :这个是默认的原子顺序,即按代码怎么写的就是怎么个顺序!

memory_order_relaxed:这个是松散顺序,《C++并发编程实战》第5章 123页举的例子讲的很清楚,鉴于篇幅,我也简单陈述一下,举书本上的例子:

#include <atomic>
#include <thread>
#include <assert.h>

std::atomic<bool> x,y;
std::atomic<int> z;

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
  if(x.load(std::memory_order_relaxed)) #4
    ++z;
}

int main(){
  x=false;
  y=false;
  z=0;
  std::thread a(write_x_then_y);
  std::thread b(read_y_then_x);
  a.join();
  b.join();
  assert(z.load()!=0); #5//断言发生,z是可能等于0的
}

 

为什么断言可能发生?意思是z可能为0,x可能为0,这个问题,就是relaxed的锅, 在write_x_then_y线程中,因为#1,#2的store是松散的,在read_y_then_x线程中,也是以松散来读的,x与y没有必然的联系,意思是x.load的时候,可能返回false,编译器或者硬件可随便更改线程中的顺序,所以说慎用使用松散顺序!还有就是这种是理想条件下的,至少x86Cpu目前没有该功能!

memory_order_release,memory_order_acquire:我把这个放在一起,因为这两个是一套的,要搭配使用,举个例子来

#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x,y;
std::atomic<int> z;


void write_x(){   x.store(true,std::memory_order_release); }

void write_y(){   y.store(true,std::memory_order_release); }

void read_x_then_y(){   while(!x.load(std::memory_order_acquire));   if(y.load(std::memory_order_acquire))//y为false;     ++z; }

void read_y_then_x(){   while(!y.load(std::memory_order_acquire));   if(x.load(std::memory_order_acquire))//x为false;     ++z; }

int main(){   x=false;   y=false;   z=0;   std::thread a(write_x);   std::thread b(write_y);   std::thread c(read_x_then_y);   std::thread d(read_y_then_x);   a.join();   b.join();   c.join();   d.join();   assert(z.load()!=0); }

 



z这次会触发,意思是z=0,原因4个线程相互独立,release与acquire是一种同步的搭配,但他们必须配对,如果不配对,就像relaxed一样,返回先前的值。

memory_order_release,memory_order_consume 这两个也是一对的,配对使用才能同步,与acquire区别在这里

struct X{
  int i;
  std::string s;
};

std::atomic
<X*> p; std::atomic<int> a;
void create_x(){   X* x=new X;   x->i=42;   x->s=”hello”;   a.store(99,std::memory_order_relaxed); // 1   p.store(x,std::memory_order_release); }
void use_x(){   X* x;   while(!(x=p.load(std::memory_order_consume))) // 2   std::this_thread::sleep(std::chrono::microseconds(1));   assert(x->i==42);   assert(x->s==”hello”);   assert(a.load(std::memory_order_relaxed)==99); //3 }

int main(){   std::thread t1(create_x);   std::thread t2(use_x);   t1.join();   t2.join(); }

 

i++和++i是否为原子操作

一.i++

i++的操作分三步:

(1)栈中取出i

(2)i自增1

(3)将i存到栈

所以i++不是原子操作,上面的三个步骤中任何一个步骤同时操作,都可能导致i的值不正确自增

二.++i

在多核的机器上,cpu在读取内存i时也会可能发生同时读取到同一值,这就导致两次自增,实际只增加了一次。

 

综上,我认为i++和++i都不是原子操作。

 

————————————————
版权声明:本文为CSDN博主「小乌龟在大乌龟背上」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/what951006/article/details/78273903

posted @ 2021-08-26 11:22  默行于世  阅读(514)  评论(0编辑  收藏  举报