自己開發了一個股票軟件,功能很強大,需要的點擊下面的鏈接獲取:
https://www.cnblogs.com/bclshuai/p/11380657.html
目錄
1 介紹... 1
1.1 原子操作... 1
1.2 指令執行順序... 2
1.3 編譯器和CPU指令重排... 2
1.4 依賴關系... 3
1.5 memoryorder作用... 3
2 六種內存模式... 3
2.1 Relaxed ordering. 5
2.2 Release – acquire. 6
2.3 Release – consume. 7
2.4 memory_order_acq_rel 10
2.5 Sequentially-consistent ordering. 11
2.6 總結... 13
3 參考文獻... 14
1 介紹
多線程編程已經是大家熟知的知識,多線程編程主要的問題就是多線程同時訪問一個變量時,會造成同時讀寫同一個變量的問題,造成數據異常,通常會使用mutex、臨界區、條件變量來實現多線程同步,避免多線程同時讀寫同一個變量。但是鎖競爭也會影響程序的執行效率,所以C++11引入了原子變量atomic,實現變量的原子操作,線程對變量的操作馬上對其他線程可見,避免使用鎖臨界區造成的消耗。同時C++11引入了內存模型,可以同步不同線程之間的原子變量操作前后的代碼的重排限制。
1.1 原子操作
首先,什么是原子操作?原子操作就是對一個內存上變量(或者叫左值)的讀取-變更-存儲(load-add-store)作為一個整體一次完成。例如x++這個表達式如果編譯成匯編,對應的是3條指令:mov(從內存到寄存器),add,mov(從寄存器到內存)那么在多線程環境下,就存在這樣的可能:當線程A剛剛執行完第二條add指令的時候,還沒有執行第三條mov指令,線程B就同時開始執行第一條指令。那么B讀到的數據還是0,A執行第三條指令后寫入內存,x值是1,B再執行第三條指令從寄存器寫到內存,x還是1。如果是原子操作,mov(從內存到寄存器),add,mov(從寄存器到內存)這三條指令必須一次完成,線程A執行三條之后,x=1,線程B在執行三條指令,得到x=2。atomic本身就是一種鎖,它自己就已經完成這種原子操作的作用。內存順序是控制不同原子操作之間的執行順序,比如是線程B先加1還是線程A先加1。
1.2 指令執行順序
為了盡可能地提高計算機資源利用率和性能,編譯器會對代碼進行重新排序, CPU 會對指令進行重新排序、延緩執行、各種緩存等等,以達到更好的執行效果。單線程則是按照線程中的代碼順序執行指令,多線程時,兩個線程之間執行指令的順序會進行優化調整,所以無法保證兩個線程中指令執行的相對順序,例如線程1有指令A,B,線程2有指令C,D,兩個線程同時執行,那么可能的執行順序是ABCD,ACBD,CDAB,CABD等;所以C++11引用內存順序操作,實現控制多線程中指令執行順序,實現多線程同步。能夠按照你想的順序執行指令。
happens-before關系,說白了就是代碼編寫順序,一般是指單線程內部的代碼順序。Synchronized-with關系則是多線程之間的同步關系,通過6個模式,實現多線程中指向執行順序的不同約束。
1.3 編譯器和CPU指令重排
代碼順序:就是你按照代碼一行一行從上往下的順序;
編譯器對代碼可能進行指令重排。也就是編譯生成的二進制(機器碼)的順序與源代碼可能不同,例如一個線程中有兩行代碼x++;y++;雖然y++在x++之后,但是編譯器可能會把y++放到x++之前。而且CPU內部也有指令重排,也就是說,CPU執行指令的順序,也不見得是完全嚴格按照機器碼的順序。當代CPU的IPC(每時鍾執行指令數)一般都遠大於1,也就是所謂的多發射,很多命令都是同時執行的。比如,當代CPU當中(一個核心)一般會有2套以上的整數ALU(加法器),2套以上的浮點ALU(加法器),往往還有獨立的乘法器,以及,獨立的Load和Store執行器。Load和Store模塊往往還有8個以上的隊列,也就是可以同時進行8個以上內存地址(cache line)的讀寫交換。
1.4 依賴關系
單線程中指令重排也不會亂排,不相關的指令可以重排,相關的指令不能重排;例如線程1中有兩條指令x++;y++;這兩條指令是完全不相關的,可以任意調整順序。但是如果是x++;y=x;那這兩條指令是依賴關系,那么一定是按照代碼順序去執行。
1.5 memoryorder作用
memory order,其實就是限制編譯器以及CPU對單線程當中的指令執行順序進行重排的程度(此外還包括對cache的控制方法)。這種限制,決定了以atomic操作為基准點(邊界),對其之前后的內存訪問命令,能夠在多大的范圍內自由重排(或者反過來,需要施加多大的保序限制),也被稱為柵欄。從而形成了6種模式。它本身與多線程無關,是限制的單一線程當中指令執行順序。
參考文獻
https://www.zhihu.com/question/24301047
2 六種內存模式
編號 |
順序關系 |
說明 |
1 |
Relaxed order限制相關變量的原子操作。 |
只保證線程1中的g.Store和線程2中的g.load操作是原子操作。不保證線程之間的g操作指令同步順序。也不限制其他變量的順序。 |
2 |
Release-acquire同步多線程順序,強制其他變量的順帶關系。 |
(1) 線程1中,g.store(release)之前讀寫操作不允許重排到g.store(release)后面。 (2) g.load(acquire)之后的讀寫操作不允許被重排到g.load(acquire)之前。 (3) 如果g.store()在gload()之前執行,那么g.store(release)之前的所有寫操作對g.load(acquire)之后的命令可見。 |
3 |
Release-consume只同步同步順序,強制其他變量的順帶關系。 |
(1) 只保證原子操作,不會影響非依賴關系變量的重排順序限制。 (2) 對有依賴關系的變量,如果g.store()在gload()之前執行,限制g.store(release)之前的所有寫操作對g.load(acquire)之后的命令可見。 |
4 |
memory_order_acq_rel: |
(1)在當前線程對讀取和寫入施加 acquire-release 語義,語句后的不能重排到前面,語句前的不能重排到后面。 (2)可以看見其他線程施加 release 語義之前的所有寫入,同時自己的 release 結束后所有寫入對其他施加 acquire 語義的線程可見。 |
5 |
memory_order_seq_cst |
順序一致性模型,(1)對變量施加acq_rel語義限制的限制,(2)同時還建立一個對所有原子變量操作的全局唯一修改順序,所有線程看到的內存操作的順序都是一樣的。 |
2.1 Relaxed ordering
Relaxed ordering,放松的排序,只保證操作是原子操作,但是不保證任何順序,單線程中除了依賴關系的按照代碼順序,沒有依賴關系的則排序任意。舉個例子。如下建立兩個原子變量,線程1中執行賦值操作A,B,線程2中執行讀取操作C,D。因為Relaxed ordering只保證操作A,B,C,D是原子操作,A,B之間沒有依賴關系,C,D之間也沒有依賴關系,所以線程1中執行順序可以是A,B,也可以是B,A,線程2中執行順序可以是C,D,也可以是D,C,線程1和線程2之間也沒有任何同步關系,所以線程1和線程2同時執行時,A,B,C,D可以是任意順序,如果D在A之前執行,例如執行順序是B,D,C,A,則D指令斷言會出現失敗,因為A操作還沒有寫入f為true。例子中C操作的 while循環,只是保證B操作執行完。實際應用中可以不用循環。
atomic<bool> f=false;
atomic<bool> g=false;
// thread1
f.store(true, memory_order_relaxed);//A
g.store(true, memory_order_relaxed);//B
// thread2
while(!g.load(memory_order_relaxed));//C
assert(f.load(memory_order_relaxed));//D
如果存在依賴關系,把B改成g.store(f, memory_order_relaxed);,g依賴於f,則線程1中執行順序只能是A,B,線程2中還是任意順序CD,或者DC。線程1和線程2中執行順序還是任意順序,只是A必須在B前面。可以是ABCD、ACBD,DABC等;D還是有可能在A之前執行,所以D還是會出現斷言失敗。怎么保證A一定在D之前執行,讓斷言不失敗呢,也是要控制兩個線程中的兩條指令的順序,可以使用Release – acquire順序關系來實現。
2.2 Release – acquire
多線程並發是為了提高效率,多線程同步是為了解決同時訪問同一個變量的問題,線程1中g.store(release)寫變量和線程2中g.load(acquire)讀變量組合使用,並不是保證g.store(release)一定在g.load(acquire)之前執行,如果線程1一直sleep幾秒,線程2會執行g.load(acquire)命令。這里的同步是指線程1中g.store(release)之前讀寫不能被重排到g.store(release)之后,線程2 g.load(acquire)之后的讀寫不能被重排到g.load(acquire)之前,如果g.store(release)先於g.load(acquire)之前執行(前提),那么線程1中g.store(release)之前的讀寫對線程2中g.load(acquire)之后的讀寫可見。如果g.load(acquire)先於g.store(release)之前執行,那么無法保證線程1中g.store(release)之前的讀寫對線程2中g.load(acquire)之后的讀寫可見。總結三點如下:
(1) load(acquire)所在的線程中load(acquire)之后的所有寫操作(包含非依賴關系),不允許被移動到這個load()的前面,一定在load之后執行。
(2) store(release)之前的所有讀寫操作(包含非依賴關系),不允許被重排到這個store(release)的后面,一定在store之前執行。
(3) 如果store(release)在load(acquire)之前執行了(前提),那么store(release)之前的寫操作對 load(acquire)之后的讀寫操作可見。
例如
bool f=false;
atomic<bool> g=false;
// thread1
f=true//A
g.store(true, memory_order_release);//B
// thread2
while(!g.load(memory_order_ acquire));//C
assert(f));//D
根據規則(1),線程1中A不允許被重排到B之后,根據規則(2)D不允許被重排到C之前,根據規則(3),因為C中有while循環,一直等待,等到B執行完了,C中循環才退出,保證B在C之前執行完,A又一定在B之前執行完,那么D讀到就永遠是true,永遠不會失敗。如果C沒 循環,即使加了release和acquire,也不能保證B在C之前執行,D也可能會出現失敗。
release -- acquire 有個牛逼的副作用:線程 1 中所有發生在 B 之前的A操作,都會在B之前執行,D也一定在C之后執行,A,D好像很無辜,無緣無故的就被強制順序了。如果不想讓A,D被順帶強制順序,可以使用Release – consume。
2.3 Release – consume
(1)Release – consume實例
Release – consume也是實現多線程之間指令的同步問題,與Release – acquire不同的是,Release – consume不會限制線程中其他變量的順序重排,不會順帶強制其前后其他指令(無依賴關系)的順序。避免了其他指令強制順序帶來的額外開銷。例如:
bool f=false;
atomic<bool> g=false;
// thread1
f=true//A
g.store(true, memory_order_release);//B
// thread2
while(!g.load(memory_order_consume);//C
assert(f));//D
同樣的例子例子中使用了release和consume關系,不會限制A、B和C、D指令的順序,可以任意重排,線程1中可以是AB,BA,線程2中可以是CD,DC。線程1和線程2可以是任意的排列組合。所以D有可能斷言失敗。這種情況和relax是一樣的。
(2)Release – consume依賴關系變量限制重排
有依賴關系的變量的指令順序還是會按照代碼順序去執行,如果AB之間有依賴關系例如下面的例子:
bool f=false;
atomic<bool> g=false;
// thread1
f=true//A
g.store(f, memory_order_release);//B g依賴於f
// thread2
while(!g.load(memory_order_consume);//C
assert(f));//D
因為B中的變量g依賴於f,所以線程1中指令順序只能是AB,線程2中D一定成功,因為在線程1中g依賴於f,所以A一定在B之前執行,線程2中D也被限制不能重排到C之前,C中的while循環會一直等到g變為true,說明f已經為true,那么D永遠成功。
(3)relax和consume的區別
那么relax和consume不是一樣嗎?都是線程中有依賴關系就按照代碼順序。否則可以任意排序,relax和consume的區別是什么?如下面的例子所示,將release和consume都換成relax。
bool f=false;
atomic<bool> g=false;
// thread1
f=true//A
g.store(f, memory_order_relax);//B g依賴於f
// thread2
while(!g.load(memory_order_ relax);//C
assert(f));//D
線程1中g依賴於f,所以按照代碼順序,A在B之前執行。因為在線程2中CD之間沒有依賴關系,所以線程2中CD可以任意重排。而如果是consume,那么線程2中就只能是CD順序,不能被重排。因為線程1中依賴關系也影響了線程2中的指令重排限制,線程中B之前的依賴變量寫入對線程2中C之后的依賴變量的讀取可見。這就是relax和consume的區別。
2.4 memory_order_acq_rel
對讀取和寫入施加 acquire-release 語義,也就是g.store(acquire-release)或者g.load(acquire-release)前面無法被重排到后面,后面無法被重排到前面。
可以看見其他線程施加 release 之前的所有寫入,同時自己之前所有寫入對其他施加 acquire 語義的線程可見。例如下面的例子:
bool f=false;
atomic<bool> g=false;
bool h=false;
// thread1
f=true//A
g.store(true, memory_order_release);//B
// thread2
while(!g.load(memory_order_ acquire);//C
assert(f));//D
assert(h);//E
//thread3
h=true;//F
while(!g.load(memory_order_acq_rel);//G
assert(f));//H
根據規則,線程1中A操作不允許被重排到B之后,線程2中DE操作不允許被重排到C之前。線程3中F操作不允許被重排到G之后,H操作不允許被重排到G之前。
線程1中A操作寫入對線程2中D讀取以及線程3中H操作的讀取都是可見,即DH在g為true的前提下,讀到的一定是true;同時線程3中F操作的寫入對線程2中E操作的讀取可見,即E操作在g為true的前提下,讀到的一定是true。
2.5 Sequentially-consistent ordering
默認情況下,std::atomic使用的是 Sequentially-consistent ordering,除了包含release/acquire的限制,同時還建立一個對所有原子變量操作的全局唯一修改順序。即采用統一的全局順序,所有的線程看到的順序是一致的。會在多個線程間切換,達到多個線程仿佛在一個線程內順序執行的效果。即單線程中按照代碼順序,多線程之間按照一個全局統一順序,具體什么順序按照時間片的分配。
舉例如下
// 順序一致
std::atomic<bool> x,y;
std::atomic<int> z;
void write_x()
{
x.store(true,std::memory_order_seq_cst);//A
}
void write_y()
{
y.store(true,std::memory_order_seq_cst);//B
}
void read_x_then_y()
{
while(!x.load(std::memory_order_seq_cst));//C
if(y.load(std::memory_order_seq_cst))//D
++z;
}
void read_y_then_x()
{
while(!y.load(std::memory_order_seq_cst));//E
if(x.load(std::memory_order_seq_cst))F
++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);
}
上面一共四個線程,假如四個線程同時啟動,那ABCDEF6條指令按照什么順序執行呢?四個線程並發執行,都可能先執行,總的全局順序會選擇下圖中的一條環線順序開始執行,而且對所有的線程來說都是按照這個全局順序執行。
例如按照ACDBEF的順序執行,假如線程write_x先分配到時間片,A先執行,x變為true,線程read_x_then_y中C操作while循環退出,D操作執行,B執行,y變為true,E中while循環退出,執行F。
再比如ABCDEF,ACBEDF等,只是C一定在D之前,E一定在F之前。
2.6 總結
六種模型參數本質上是限制單線程內部的指令重排順序,並不是同步不同線程之間的指令順序,而是通過限制單線程中指令的重排,以控制帶有模型參數的變量前后的指令被重排順序限制。這種限制,決定了以atomic操作為基准點(邊界),對其之前的內存訪問命令,以及之后的內存訪問命令,能夠在多大的范圍內自由重排。上面的例子中,使用while循環,來一直等待,是為了保證store為true后,load為true,從而退出while循環,因為store之前的寫指令在store之前完成,所以store之前的寫指令對while(load(acquire))之后的寫指令可見,while循環一直等待,強制了多線程間兩個指令的順序,這樣寫只是為了說明原理,實際應用中不會這樣去編程。
3 參考文獻
https://www.zhihu.com/question/24301047
https://en.cppreference.com/w/cpp/atomic/memory_order
https://blog.csdn.net/lvdan1/article/details/54098559
https://zhuanlan.zhihu.com/p/45566448
http://senlinzhan.github.io/2017/12/04/cpp-memory-order/