1. 程式人生 > >C++ 原子操作(6種原子順序)

C++ 原子操作(6種原子順序)

一、我們要先搞明白什麼叫原子操作?使用這個東西有什麼目的?

原子操作:能夠操作最接近機器的指令,這個和硬體相關了,雖然和硬體相關,但我們的C11還是整合了這一切,讓原子操作有了共同的呼叫介面
目的:使用這個的目的說實話,就是讓你更瞭解機器已及多執行緒同步的原理和祕密,當然有一些需求較簡單的,使用原子操作可能比封裝好的更有效率!!用了百遍的mutex可能你現在還不知道他們是怎麼互斥的~當然內部還是通過了原子操作來的!

二、講講原理

原子操作只有2種狀態,一種是沒做,一種是做完了,看不到正在做的狀態,這個是在任何執行緒下都滿足這個要求,當兩個執行緒同時訪問一塊記憶體的時候,如果有任何一個在寫,那肯定會產生競爭,如果兩個同時讀,沒有問題,那如何用原子操作來控制不產生競爭呢?可以這樣來想,當兩個執行緒在訪問的時候,一定有一個先後順序,誰先訪問,誰後訪問,這就是修改順序,我們要在任何執行緒可以看到這樣的順序!然後就可以通過一定的邏輯來處理併發競爭的情況了!

三、標準庫操作

例:我們一實現一個執行緒讀取,一個執行緒寫入,當然,要先寫入才能讀取,所以這個是順序問題。

方法一:condition_variable來操作

/*
使用condition_variable,讀執行緒wait,直到寫執行緒呼叫 notify_all,即停止等待
*/
#include <thread>
#include <condition_variable>
#include <iostream>
using namespace std;

condition_variable g_CV;
mutex g_mtx;
void
read_thread() { while (true) { unique_lock<mutex> ul(g_mtx); g_CV.wait(ul); cout << g_value << endl; } } void write_thread() { while (true) { Sleep(1000); lock_guard<mutex> lg(g_mtx); g_value++; g_CV.notify_all(); } } int
main() { thread th(read_thread); th.detach(); thread th2(write_thread); th2.detach(); char a; while (cin >> a); return 0; }

結果:每隔1秒列印
這裡寫圖片描述

方法二:使用標準原子操作

/*使用原子操作來控制執行緒順序,實現了一個類似於互斥鎖這麼一個概念*/
#include <thread>
#include <atomic>
#include <iostream>
using namespace std;

int g_value(0);
atomic<bool> g_data_ready(false);

void read_thread()
{
    while (true)
    {
        while (!g_data_ready.load());//用於控制進入
        cout << g_value << endl;
        g_data_ready = false;
    }
}

void write_thread()
{
    while (true)
    {
        while (g_data_ready.load());//用於控制進入
        Sleep(1000);
        g_value++;
        g_data_ready = true;
    }
}

int main()
{
    thread th(read_thread);
    th.detach();
    thread th2(write_thread);
    th2.detach();
    char a;
    while (cin >> a);
    return 0;
}

結果:每隔1秒列印
同樣

四、六種原子操作記憶體順序

這個比較的頭痛了,對於剛接觸這個概念的時候,我也是一頭霧水。。但隨著我各種查資料,慢慢就有所理解,知乎的一位答主講的比較不錯,點此,再加之深究《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();
}

如果把位置2的consume 換成acquire,那他們是同步的,3雖然是relaxed,但被迫同步了!a.load會等於3,如果像上面這樣的程式碼,a.load不會等於3,意思是consume的release的前不保證happend-before關係。我找不到更好的辦法來描述書上的內容了。