最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++中多線程的執(zhí)行順序如你預(yù)期嗎

 更新時間:2022年10月20日 14:07:36   作者:寒星n號  
這篇文章主要為大家詳細介紹一下C++中多線程的執(zhí)行順序的相關(guān)資料,文中的示例代碼講解詳細,對我們學(xué)習(xí)C++多線程有一定幫助,需要的可以參考一下

一個簡單的例子

先來看一個多線程的例子:

如圖所示,我們將變量x和y初始化為0,然后在線程1中執(zhí)行:

	x = 1, m = y;

同時在線程2中執(zhí)行:

	y = 1, n = x;

當兩個線程都執(zhí)行結(jié)束以后,m和n的值分別是多少呢?

對于已經(jīng)工作了n年、寫過無數(shù)次并發(fā)程序的的我們來說,這還不是小case嗎?讓我們來分析一下,大概有三種情況:

  • 如果程序先執(zhí)行了x = 1, m = y代碼段,后執(zhí)行了y = 1, n = x代碼段,那么結(jié)果是m = 0, n = 1;
  • 如果程序先執(zhí)行了y = 1, n = x代碼段,后執(zhí)行了x = 1, m = y代碼段,那么結(jié)果是m = 1, n = 0;
  • 如果程序的執(zhí)行順序先是 x = 1, y = 1, 后執(zhí)行m = y, n = x, 那么結(jié)果是m = 1, n = 1;

所以(m, n)的組合一共有3種情況,分別是(0, 1), (1, 0)和(1, 1)。

那有沒有可能程序執(zhí)行結(jié)束后,(m, n)的值是(0, 0)呢?嗯...我們又仔細的回顧了一下自己的分析過程:在m和n被賦值的時候,x = 1和y = 1至少有一條語句被執(zhí)行了...沒有問題,那應(yīng)該就不會出現(xiàn)m和n都是0的情況。

詭異的輸出結(jié)果

不過人在江湖上混,還是要嚴謹一點。好在這代碼邏輯也不復(fù)雜,那就寫一段簡單的程序來驗證下吧:

#include <iostream>
#include <thread>

using namespace std;

int x = 0, y = 0, m = 0, n = 0;
int main()
{
	while (1) {
		x = y = 0;
		thread t1([&]() { x = 1; m = y; });
		thread t2([&]() { y = 1; n = x; });
		t1.join(); t2.join();

		if (m == 0 && n == 0) {
			cout << " m == 0 && n == 0 ? impossible!\n";
		}
	}
	return 0;
}

考慮到多線程的隨機性,就寫一個無限循環(huán)多跑一會吧,反正屏幕也不會有什么輸出。我們信心滿滿的把程序跑了起來,但很快就發(fā)現(xiàn)有點不太對勁:

m和n居然真的同時為0了?不可能不可能...這難道是windows或者msvc的bug?那我們到linux上用g++編譯試一下,結(jié)果程序跑起來之后,又看到了熟悉的輸出:

這...打臉未免來得也太快了吧!

你看到的執(zhí)行順序不是真的執(zhí)行順序

看來這不是bug,真的是有可能出現(xiàn)m和n都是0的情況。可是,到底是為什么呢?恍惚之間,我們突然想起曾經(jīng)似乎在哪看過這樣一個as-if規(guī)則:

The rule that allows any and all code transformations that do not change the observable behavior of the program.

也就是說,在不影響可觀測結(jié)果的前提下,編譯器是有可能對程序的代碼進行重排,以取得更好的執(zhí)行效率的。比如像這樣的代碼:

int a, b;
void test()
{
	a = b + 1;
	b = 1;
}

編譯器是完全有可能重新排列成下面的樣子的:

int a, b;
void test()
{
	int c = b;
	b = 1;
	c += 1;
	a = c;
}

這樣,程序在實際執(zhí)行過程中對a的賦值就晚于對b的賦值之后了。不過,有了前車之鑒,我們還是先驗證一下在下結(jié)論吧。我們使用gcc的-S選項,生成匯編代碼(開啟-O2優(yōu)化)來看一下,編譯器生成的指令到底是什么樣子的:

哈哈,果然如我們所料,對a的賦值被調(diào)整到對b的賦值后面了!那上面m和n同時為0也一定是因為編譯器重新排序我們的指令順序?qū)е碌?!想到這里,我們的底氣又漸漸回來了。那就生成匯編代碼看看吧:

果然不出所料,因為我們在編譯的時候開了-O3優(yōu)化,賦值的順序被重排了!代碼實際的執(zhí)行順序大概是下面這個樣子:

	int t1 = y; x = 1; m = t1; //線程1
	int t2 = x; y = 1; n = t2; //線程2

這就難怪會出現(xiàn)m = 0, n = 0這樣的結(jié)果了。分析到這里,我們終于有點松了一口氣,這多年的編程經(jīng)驗可不是白來的,總算是給出了一個合理的解釋。
那我們在編譯的時候把-O3優(yōu)化選項去掉,盡量讓編譯器不要進行優(yōu)化,保持原來的指令執(zhí)行順序,應(yīng)該就可以避免m和n同時為0的結(jié)果了吧?試試,保險起見,我們還是先看一看匯編代碼吧:

跟我們的預(yù)期一致,匯編代碼保持了原來的執(zhí)行順序,這回肯定沒有問題了。那就把程序跑起來吧。然而...不一會兒,熟悉的打印又出現(xiàn)了...

這...到底是怎么回事??。?!

你看到的執(zhí)行順序還不是真正的執(zhí)行順序

如果不是編譯器重排了我們的指令順序,那還會是什么呢?難道是CPU?!
還真是。實際上,現(xiàn)代CPU為了提高執(zhí)行效率,大多都采用了流水線技術(shù)。例如:一個執(zhí)行過程可以被分為:取指(IF),譯碼(ID),執(zhí)行(EX),訪存(MEM),回寫(WB)等階段。這樣,當?shù)谝粭l指令在執(zhí)行的時候,第二條指令可以進行譯碼,第三條指令可以進行取指...于是CPU被充分利用了,指令的執(zhí)行效率也大大提高。一個標準的5級流水線的工作過程如下表所示(實際的CPU流水線遠比這復(fù)雜得多):

序號/時鐘周期1234567...
1IFIDEXMEMWB   
2 IFIDEXMEMWB  
3  IFIDEXMEMWB 
4   IFIDEXMEMWB
5    IFIDEXMEM
6     IFIDEX

上面展示的指令流水線是完美的,然而實際情況往往沒有這么理想??紤]這樣一種情況,假設(shè)第二條指令依賴于第一條指令的執(zhí)行結(jié)果,而第一條指令恰巧又是一個比較耗時的操作,那么整個流水線就停止了。即使第三條指令與前兩條指令完全無關(guān),它也必須等到第一條指令執(zhí)行完成,流水線繼續(xù)運轉(zhuǎn)時才能得已執(zhí)行。這就浪費了CPU的執(zhí)行帶寬。亂序執(zhí)行(Out-Of-Order Execution)就是被用來解決這一問題的,它也是現(xiàn)代CPU提升執(zhí)行效率的基礎(chǔ)技術(shù)之一。
簡單來說,亂序執(zhí)行是指CPU提前分析待執(zhí)行的指令,調(diào)整指令的執(zhí)行順序,以期發(fā)揮更高流水線執(zhí)行效率的一種技術(shù)。引入亂序執(zhí)行技術(shù)以后,CPU執(zhí)行指令過程大概是下面這個樣子:

所以,上面的程序出現(xiàn)(m, n)結(jié)果為(0, 0)的情況,應(yīng)該就是因為指令的執(zhí)行順序被CPU重排了!

C++多線程內(nèi)存模型

我們通常將讀取操作稱為load,存儲操作稱為store。對應(yīng)的內(nèi)存操作順序有以下幾種:

  • load->load(讀讀)
  • load->store(讀寫)
  • store->load(寫讀)
  • store->store(寫寫)

CPU在執(zhí)行指令的時候,會根據(jù)情況對內(nèi)存操作順序進行重新排列。也就是說,我們只要能夠讓CPU不要進行指令重排優(yōu)化,那么應(yīng)該就不會出現(xiàn)(m, n)為(0, 0)的情況了。但具體要怎么做呢?

實際上,在C++11之前,我們很難在語言層面做到這件事情。那時的C++甚至連線程都不支持,更別提什么內(nèi)存模型了。在C++98的年代,我們只能通過嵌入?yún)R編的方式添加內(nèi)存屏障來達到這樣的目的:

asm volatile("mfence" ::: "memory");

不過在現(xiàn)代C++中,要做這樣的事情就簡單多了。C++11引入了原子類型(atomic),同時規(guī)定了6種內(nèi)存執(zhí)行順序:

  • memory_order_relaxed: 松散的,在保證原子性的前提下,允許進行任務(wù)的重新排序;
  • memory_order_release: 代碼中這條語句前的所有讀寫操作, 不允許被重排到這個操作之后;
  • memory_order_acquire: 代碼中這條語句后的所有讀寫操作,不允許被重排到這個操作之前;
  • memory_order_consume: 代碼中這條語句后所有與這塊內(nèi)存相關(guān)的讀寫操作,不允許被重排到這個操作之前;注意,這個類型已不建議被使用;
  • memory_order_acq_rel: 對讀取和寫入施加acquire-release語義,無法被重排;
  • memory_order_seq_cst: 順序一致性,如果是寫入就是release語義,如果是讀取是acquire語義,如果是讀取-寫入就是acquire-release語義;也是原子變量的默認語義。

所以,我們只需要將x和y的類型改為atmioc_int,就可以避免m和n同時為0的結(jié)果出現(xiàn)了。修改后的代碼如下:

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

using namespace std;

atomic_int x(0);
atomic_int y(0);
int m = 0, n = 0;
int main()
{
        while (1) {
                x = y = 0;
                thread t1([&]() { x = 1; m = y; });
                thread t2([&]() { y = 1; n = x; });
                t1.join(); t2.join();

                if (m == 0 && n == 0) {
                        cout << " m == 0 && n == 0 ? impossible!\n";
                }
        }
        return 0;
}

現(xiàn)在編譯運行一下,看看結(jié)果:

已經(jīng)不會再出現(xiàn)"impossible"的打印了。我們再來看看生成的匯編代碼:

原來編譯器已經(jīng)自動幫我們插入了內(nèi)存屏障,這樣就再也不會出現(xiàn)(m, n)為(0, 0)的情況了。

到此這篇關(guān)于C++中多線程的執(zhí)行順序如你預(yù)期嗎的文章就介紹到這了,更多相關(guān)C++多線程執(zhí)行順序內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • C++中delete指針后最好將其置空的操作方法

    C++中delete指針后最好將其置空的操作方法

    C++編程中,當你使用delete運算符釋放指針所指向的內(nèi)存后,通常將該指針置空,如果一個指針在被刪除后沒有置空,而你在代碼的其他部分再次嘗試刪除同一個指針,可能會導(dǎo)致程序崩潰或產(chǎn)生未定義行為,本文介紹C++中delete指針后最好將其置空的操作方法,感興趣的朋友一起看看吧
    2024-06-06
  • C語言由淺入深講解線程的定義

    C語言由淺入深講解線程的定義

    這篇文章主要介紹了C語言中線程的基礎(chǔ)知識,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-05-05
  • C++ 先對數(shù)組排序,在進行折半查找

    C++ 先對數(shù)組排序,在進行折半查找

    以下小編就為大家介紹兩種實現(xiàn)方法。第一種方法是,選擇排序法+循環(huán)折半查找法。第二種方法是,冒泡排序法+遞歸折半查找法。需要的朋友可以過來參考下,希望對大家有所幫助
    2013-10-10
  • C++中的六個函數(shù)

    C++中的六個函數(shù)

    本文給大家介紹了C++中的六個函數(shù),非常不錯,具有一定的參考借鑒價值,需要的朋友參考下吧
    2018-05-05
  • 使用WindowsAPI實現(xiàn)播放PCM音頻的方法

    使用WindowsAPI實現(xiàn)播放PCM音頻的方法

    這篇文章主要介紹了使用WindowsAPI實現(xiàn)播放PCM音頻的方法,很實用的一個功能,需要的朋友可以參考下
    2014-08-08
  • C語言的變量類型及內(nèi)存大小詳解

    C語言的變量類型及內(nèi)存大小詳解

    這篇文章主要介紹了CC和C++變量類型及內(nèi)存大小,是C++入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下,希望能夠給你帶來幫助
    2021-09-09
  • 雙緩沖解決VC++繪圖時屏幕閃爍

    雙緩沖解決VC++繪圖時屏幕閃爍

    相信很多人在做圖形界面開發(fā)時,常常會遇到屏幕閃爍的情況,當然我也不例外,下面我們就來詳細探討下這個問題的解決辦法
    2015-08-08
  • C++函數(shù)重載的定義與原因詳解

    C++函數(shù)重載的定義與原因詳解

    這篇文章主要為大家詳細介紹了Python實現(xiàn)學(xué)生成績管理系統(tǒng),使用數(shù)據(jù)庫,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • C語言輸出旋轉(zhuǎn)后數(shù)組中的最小數(shù)元素的算法原理與實例

    C語言輸出旋轉(zhuǎn)后數(shù)組中的最小數(shù)元素的算法原理與實例

    這篇文章主要介紹了C語言輸出旋轉(zhuǎn)后數(shù)組中的最小數(shù)元素的算法原理與實例,數(shù)組旋轉(zhuǎn)就是把開頭的幾個指定的元素放到數(shù)組的末尾,需要的朋友可以參考下
    2016-03-03
  • C++ 實現(xiàn)的通訊錄管理系統(tǒng)詳解

    C++ 實現(xiàn)的通訊錄管理系統(tǒng)詳解

    這篇文章主要為大家詳細介紹了C++實現(xiàn)通訊錄管理系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-10-10

最新評論

华亭县| 涿州市| 贡觉县| 泽库县| 政和县| 芦山县| 汉阴县| 伊金霍洛旗| 普宁市| 奇台县| 平陆县| 青冈县| 南靖县| 罗甸县| 革吉县| 大关县| 彭州市| 呈贡县| 乡城县| 惠来县| 原阳县| 临海市| 吉林省| 北辰区| 祁连县| 广南县| 云梦县| 嘉祥县| 镇赉县| 伊吾县| 绵阳市| 社旗县| 十堰市| 库伦旗| 洛扎县| 修水县| 平山县| 阳江市| 阿巴嘎旗| 镇原县| 衡山县|