最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會(huì)員登陸 & 注冊

第 64 講:多線程(六):volatile 關(guān)鍵字

2021-10-21 21:58 作者:SunnieShine  | 我要投稿

今天我們來說一個(gè)新的 C# 的關(guān)鍵字:volatile。這個(gè)關(guān)鍵字在 C 語言里就有,不過因?yàn)?C 語言基本上也遇不到這個(gè)關(guān)鍵字的使用,所以即使是有,在課堂上也沒有辦法學(xué)到這里。

C# 的 volatile 關(guān)鍵字和 C 語言的這個(gè)關(guān)鍵字的使用場景基本一致,但因?yàn)槭敲嫦驅(qū)ο蟮恼Z言,所以也有一點(diǎn)不同。

Part 1 引例

我們來看一個(gè)超級簡單的程序。

首先,因?yàn)?x 位于類里作為字段出現(xiàn),所以它會(huì)被系統(tǒng)自動(dòng)初始化為 0。在運(yùn)行 Main 里的時(shí)候,x 會(huì)被賦值為 1。我現(xiàn)在更新后的數(shù)值會(huì)直接被 Console.WriteLine 方法調(diào)用并且輸出,所以預(yù)期結(jié)果肯定是 1。是的,編譯器也是這么想的。所以編譯器會(huì)認(rèn)為預(yù)期結(jié)果就一定是輸出 1,于是編譯器會(huì)對其進(jìn)行優(yōu)化。正因?yàn)槿绱?,編譯器會(huì)因?yàn)槟氵@個(gè)預(yù)期結(jié)果而直接去假設(shè) x 的最終結(jié)果一定是 1,然后在優(yōu)化代碼后,直接改寫代碼,改成這樣:

即直接輸出 1。這一點(diǎn)沒有問題,很好理解,對吧。

注意代碼。優(yōu)化后的代碼甚至你看不到 x 的賦值,而直接是把 1 當(dāng)成結(jié)果直接給輸出的。這個(gè)是優(yōu)化后的情況。

你可能會(huì)覺得,編譯器這么做雖然沒用 x 了,但也沒問題,對吧。與其我直接計(jì)算出結(jié)果后顯示結(jié)果,還不如優(yōu)化掉代碼,直接在編譯期間就把這個(gè)運(yùn)行結(jié)果給算出來,然后直接用常量替換掉一大堆的復(fù)雜表達(dá)式??蓡栴}在于,這種優(yōu)化代碼的過程顯然是不適用于多線程的。假設(shè)我在別的線程里更改了 x 的數(shù)值,比如我在多線程里,在 x = 1 和輸出的中間,主線程時(shí)間片到了,然后開始執(zhí)行我開的線程,并變更了 x 的數(shù)值。這種“詭變”使得時(shí)間片回到主線程的時(shí)候,x 已經(jīng)不再是 1,然后輸出的結(jié)果就不應(yīng)該是 1。在多線程的世界里,這種情況是完全可以出現(xiàn)的;而由于編譯器自身的優(yōu)化效果,好家伙,x 變量都給我們優(yōu)化掉了,直接輸出個(gè) 1,于是,程序就出現(xiàn)問題了。

倘若我已經(jīng)編譯好了一個(gè)程序,它已經(jīng)優(yōu)化過了。然后我直接上去就運(yùn)行它。結(jié)果我照著代碼看,“欸,我代碼沒錯(cuò)啊,怎么結(jié)果不按常理出牌”,就會(huì)出現(xiàn)這種復(fù)雜的、還基本上沒辦法找的 bug。

Part 2 規(guī)避編譯器無法識(shí)別的賦值優(yōu)化:volatile 關(guān)鍵字

我們試著直接給字段加上 volatile 關(guān)鍵字:

這個(gè)時(shí)候,程序就不再給 x 執(zhí)行代碼優(yōu)化了。我不管是不是正常的只有一句 x = 1,還是有多線程更改 x 的數(shù)值,還是只有單線程的情況在不斷計(jì)算 x 的數(shù)值,編譯器都不再按優(yōu)化處理這個(gè)變量了。這樣的話,雖然有些時(shí)候代碼不會(huì)增加運(yùn)行效率,但保證了程序執(zhí)行的正確性和安全性,畢竟數(shù)據(jù)沒有被優(yōu)化而規(guī)避了產(chǎn)生潛在 bug 的隱蔽的錯(cuò)誤現(xiàn)象。

Part 3 volatile 關(guān)鍵字可以用在哪些地方?

一般而言,多線程為了給方法執(zhí)行的時(shí)候傳值,我們大多數(shù)情況都會(huì)考慮使用字段來解決問題。這種情況下,字段成了多線程引用變量的常客。因此,volatile 用于字段。

第二。volatile 的變量一般都非常容易操作。所謂的“容易操作”,指的是這個(gè)變量非常方便去修改它的數(shù)值。之前我們簡單說到過一個(gè)叫做 torn read 的現(xiàn)象,當(dāng)電腦位數(shù)不夠的時(shí)候,數(shù)據(jù)類型過大就會(huì)導(dǎo)致這個(gè)對象多線程拷貝的過程之中變得不再是原子的。在 64 位數(shù)的電腦上可以拷貝 long 類型大小的數(shù)據(jù)類型的數(shù)據(jù),但現(xiàn)在仍有 32 位的電腦。C# 因?yàn)榭紤]兼容性和一些安全性層面的原因,所以多線程環(huán)境下,不讓我們對于 longulong 類型有 volatile 的修飾。當(dāng)然,別的和 long 這樣都是大于等于 8 個(gè)字節(jié)的對象也都不會(huì)讓你使用 volatile 修飾,比如 double。

所以,你在官方文獻(xiàn)上查找對應(yīng)的 volatile 的資料的時(shí)候,都會(huì)告訴你有效的數(shù)據(jù)類型都有哪些。例如,在微軟官方的 C# 語言文檔里,volatile 關(guān)鍵字相關(guān)頁面的解釋是這樣的:

大概直接翻譯一下:

  • 引用類型(因?yàn)橐妙愋蛡鬟f引用,也就是底層的地址數(shù)值。它們傳參剛好不超過本機(jī)位數(shù)大?。?;

  • 指針類型(指針類型和引用類型傳參的地址在底層基本上可以說是一個(gè)東西);

  • 內(nèi)置值類型,但不包含 long、ulong、doubledecimal(因?yàn)樗鼈円呀?jīng) 8 個(gè)字節(jié)甚至于 16 個(gè)字節(jié)了);

  • 枚舉類型,它的特征數(shù)值為非 longulong 類型;

  • 泛型類型,能夠從上下文暗示出它是引用類型的情況(這個(gè)屬于泛型,這個(gè)我們現(xiàn)在不講);

  • IntPtrUIntPtr(就是安全地封裝了的 void*,所以也是地址數(shù)值)。

所以,兩點(diǎn):

  1. 必須是字段;

  2. 該字段的類型在多線程里的賦值過程不得發(fā)生 torn read 現(xiàn)象。

Part 4 volatile 是為了避免優(yōu)化存在,那為毛要放在線程同步里說?

當(dāng)然是為了契合線程同步才會(huì)放在這里說啊。下面我們來說一個(gè) volatile 廣泛使用的、也是 volatile 最正確的使用場景——模擬用戶點(diǎn)擊(或者別的什么操作)激活程序繼續(xù)運(yùn)行的模式。用戶點(diǎn)擊之后程序才會(huì)繼續(xù)運(yùn)行,否則程序會(huì)主動(dòng)卡住在這里。這個(gè)現(xiàn)象我們是不是需要線程同步?而這個(gè)模式也剛好我們可以這里說一下,它也是 lock 的一種替代方案。

我們來看這段代碼。

刪除這里面的注釋,其實(shí)代碼并不多。我使用了一個(gè)叫做 Console.ReadKey 的方法模擬線程卡?。哼@個(gè)方法的作用原本是等待用戶按下任意按鍵以繼續(xù)執(zhí)行后面的步驟的。

請仔細(xì)查看里面的注釋文字,特別是 AnotherMethod 里的。注釋文字是我自己寫的,如果你有一點(diǎn)英語水平可以看英文(我也建議你看英文,以后查資料就不費(fèi)勁了)。大概翻譯一下是這樣的:

這段代碼用來模擬一個(gè)現(xiàn)象,模擬用戶按下任何一個(gè)按鍵才會(huì)繼續(xù)執(zhí)行后續(xù)的代碼。如果任何一個(gè)人按下了按鍵,這個(gè)程序就會(huì)繼續(xù)執(zhí)行后續(xù)的代碼,然后更改 ShouldExit 的變量的數(shù)值,改成 true。

我們這個(gè)程序的目的就是為了終止它(先運(yùn)行然后按按鍵后結(jié)束程序運(yùn)行),所以我們總會(huì)發(fā)現(xiàn)一點(diǎn),就是最終情況下的 ShouldExit 變量一定是在結(jié)束的時(shí)候?yàn)?true 的。正因?yàn)槿绱?,一個(gè)叫做 JIT 的東西(JIT 是 C# 運(yùn)行環(huán)境的優(yōu)化代碼的工具,JIT 全稱叫 Just In Time)會(huì)優(yōu)化代碼,并改寫這個(gè) ShouldExit 變量的行為,并直接在任何地方都把這個(gè)變量改成 true,就免得我任何時(shí)候讀取它的時(shí)候還去算一下結(jié)果,然后等了漫長的結(jié)果才得到這個(gè)結(jié)果 true。

換句話說,你會(huì)在優(yōu)化代碼之后發(fā)現(xiàn),這個(gè) Main 方法里的 while (!ShouldExit) 會(huì)被優(yōu)化成 while (true),顯然它已經(jīng)改寫了代碼的邏輯使得程序已經(jīng)面目全非了。所以我們?yōu)榱俗柚咕幾g器優(yōu)化它,我們要添加 volatile 關(guān)鍵字修飾這個(gè)字段。

你仔細(xì)看看這個(gè)程序的執(zhí)行流程:首先調(diào)取了線程池的線程,執(zhí)行無限期等待用戶按按鍵的過程,然后用戶按下按鍵后,直接 ShouldExit 改成 true,副線程結(jié)束;而主線程則是一直做 while 循環(huán)。很明顯主線程做循環(huán)是一直在獲取 ShouldExit 的變量數(shù)值。因?yàn)橛脩暨€沒有點(diǎn)擊繼續(xù),ShouldExit 就永遠(yuǎn)為 false,因此我使用 while (!ShouldExit) 和一個(gè)空的循環(huán)體用來占位,表示程序一直在主線程這里卡住,讓它不繼續(xù)往下執(zhí)行。

這是線程同步的第二個(gè)慣用手法:用一個(gè)無意義的死循環(huán)(無限循環(huán),或者叫近乎無限的循環(huán))來卡住線程本身,然后等待別的線程執(zhí)行運(yùn)行完成后,起到橋梁的變量 ShouldExit 改變數(shù)值后,while 循環(huán)立馬因?yàn)闂l件不成立而退出循環(huán),接著主線程就可以繼續(xù)執(zhí)行后面的代碼了。

在 UI 程序里,你可以在 while (!ShouldExit) 這樣的循環(huán)里寫上一些等待語句,比如調(diào)整控件的顯示信息,告知用戶“程序正在運(yùn)行中”,然后更改一些控件的狀態(tài)之類的??偟膩碚f,這是一種慣用法。

Part 5 它和 lockInterlocked 類型有啥關(guān)系呢?

你有沒有發(fā)現(xiàn),這個(gè)死循環(huán)有點(diǎn)像 lock 的底層邏輯:lock 的原理是開一個(gè)同步鎖來監(jiān)控線程行為。如果線程正在執(zhí)行這段代碼,別的線程就無法繼續(xù)進(jìn)行下去,直到這個(gè)線程退出關(guān)鍵代碼(同步代碼)。

我上面的 while 這個(gè)無意義的循環(huán),看起來無意義,但實(shí)際上也是在等待和卡住線程,當(dāng)這個(gè)線程運(yùn)行完畢后,其它的線程恢復(fù)執(zhí)行。是的,lock 是一個(gè)固定的語句形式,而我們有些時(shí)候也不需要一定寫成 lock 的形式,比如上面舉的這個(gè)例子。這種狀態(tài)下,我們就會(huì)覺得 while 循環(huán)更適合讀懂邏輯;與此同時(shí),這樣的代碼也有類似 lock 的效果,所以上面這種現(xiàn)象也被稱為弱同步鎖(Weak Synchronized Lock),暗示它沒有用鎖也達(dá)到了類似的效果。

Interlocked 是隱藏式的鎖,不用寫出來,所以它跟這個(gè)更加類似。大概是這么一個(gè)關(guān)系。

Part 6 道理我懂了,那么單詞為啥用 volatile?

嗯,原因很簡單。這個(gè)單詞可能你沒學(xué)過,它比較生僻,但它其實(shí)也算是比較常見的(用于日常生活的)單詞。它的意思是“易變的”、“不穩(wěn)定的”,是一個(gè)形容詞,比如說:

  • Global markets are volatile: the country's current-account surplus has fallen by more than half from a mighty 8% of GDP in just a year.

全球的市場經(jīng)濟(jì)都呈現(xiàn)出變化無常的態(tài)勢:這個(gè)國家自己的賬戶只在一年以內(nèi)就從 GDP 的 8% 的賺,減少了足足一半以上。

好吧。好像不是英語課,扯得好像有點(diǎn)多了?;氐竭@里。由于這種變量用于多線程,經(jīng)常被多個(gè)線程訪問(甚至賦值修改數(shù)據(jù)),因此這樣的變量經(jīng)常被呈現(xiàn)出不定狀態(tài),也就是說它的數(shù)值是隨時(shí)隨地都可能改變,因此翻譯成“易變的”是沒有任何問題的。但是你想想,要是用編程的視角來想這個(gè)問題,使用編程多線程的角度給這樣的特殊變量取名關(guān)鍵字的話,你會(huì)發(fā)現(xiàn)不好取名:“表示一個(gè)變量在多個(gè)線程都能訪問取值,并禁止編譯器優(yōu)化的變量”,就拿一個(gè)詞來解釋這個(gè)現(xiàn)象,換誰誰都詞窮。所以,干脆用它的現(xiàn)象“易變”作為變量的特征,定義這個(gè)關(guān)鍵字的名字的話,比較契合的同時(shí)也比較精簡,所以就用了這個(gè)詞語。

第 64 講:多線程(六):volatile 關(guān)鍵字的評論 (共 條)

分享到微博請遵守國家法律
三门峡市| 宜丰县| 贵港市| 铁力市| 兰州市| 株洲市| 宣恩县| 淮阳县| 绍兴市| 乌审旗| 阳信县| 东丽区| 阳泉市| 广灵县| 九龙城区| 井冈山市| 迭部县| 北宁市| 百色市| 三河市| 嫩江县| 嘉义县| 兴和县| 延安市| 荔浦县| 石渠县| 迁安市| 垣曲县| 临漳县| 大兴区| 双峰县| 昭平县| 大洼县| 新河县| 姜堰市| 华蓥市| 齐齐哈尔市| 浑源县| 南陵县| 石棉县| 房产|