五月天青色头像情侣网名,国产亚洲av片在线观看18女人,黑人巨茎大战俄罗斯美女,扒下她的小内裤打屁股

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

第 60 講:多線程(二):多線程的線程安全性

2021-10-06 14:33 作者:SunnieShine  | 我要投稿

多線程為了讓我們能夠有一個(gè)非常棒的代碼執(zhí)行體驗(yàn),我們可能會(huì)創(chuàng)建額外的后臺(tái)或前臺(tái)線程來和主線程配合起來并行完成同一個(gè)任務(wù)的不同部分,因?yàn)檫@么做可以“一分為多”,所以往往會(huì)讓我們覺得多線程確實(shí)比單線程(正常情況下一個(gè)程序只有一個(gè)線程,這種情況叫單線程)要快。

實(shí)際上,創(chuàng)建線程也會(huì)耗費(fèi)資源和時(shí)間,特別是線程需要記錄的東西和數(shù)據(jù)太多了??赡苣阌X得,“一句 new Thread 就可以創(chuàng)建,需要什么東西嘛”,但是要想體現(xiàn)線程的靈活用法,單純一個(gè)線程調(diào)用方法的包裝肯定是不夠的。優(yōu)先級(jí)得有吧;執(zhí)行多線程期間多線程的狀態(tài)(到底是剛開始呢,還是運(yùn)行中,還是結(jié)束了。結(jié)束又是正常結(jié)束呢,還是異常結(jié)束之類的)也得有吧。不只是這些東西,還有一些別的東西,需要今天的知識(shí)點(diǎn)才能說到。

今天我們要講解的東西是線程的安全性。什么是線程安全性呢?能夠跨線程執(zhí)行后,仍可以保證數(shù)據(jù)的有效性、正確性、有序性、一致性的時(shí)候就叫線程安全(Thread Safe)。既然都跨線程了,那么自然就會(huì)介紹主副線程的切換,這需要我們先學(xué)習(xí)一點(diǎn)操作系統(tǒng)的知識(shí)點(diǎn),今天我們探討的就是它。

本節(jié)內(nèi)容和計(jì)算機(jī)專業(yè)里面的《操作系統(tǒng)》課程的“操作系統(tǒng)的進(jìn)程調(diào)度”部分的內(nèi)容基本一致。如果你學(xué)過調(diào)度的相關(guān)內(nèi)容的話,這段內(nèi)容是可以直接跳過的。

Part 1 進(jìn)程的調(diào)度及時(shí)間片的概念

我們先來介紹一下進(jìn)程(Process)的基本概念。我們把一個(gè)數(shù)據(jù)集合(Data Collection)的一次運(yùn)行活動(dòng)稱為一個(gè)進(jìn)程。這個(gè)數(shù)據(jù)集合可能你不好掌握清楚到底范圍多大,這個(gè)是因人而異的。也就是說,數(shù)據(jù)集合可以是一個(gè)程序整體的所有數(shù)據(jù)構(gòu)成的,也可以是一個(gè)程序的其中一部分的數(shù)據(jù)。

一般而言,我們會(huì)把整個(gè)程序當(dāng)成一個(gè)數(shù)據(jù)集合,那么這個(gè)程序跑起來就是一個(gè)進(jìn)程。不過,一個(gè)程序可以包含多個(gè)線程,因此為了區(qū)別兩個(gè)概念,我們一般固定認(rèn)為,一個(gè)進(jìn)程是整個(gè)程序本身,而一個(gè)進(jìn)程可以包含多個(gè)線程,是這么個(gè)關(guān)系;而上一節(jié)內(nèi)容我們?cè)诜椒ɡ锶∶玫搅?process 這個(gè)單詞的縮寫 proc,那是因?yàn)榇_實(shí)我們也可以認(rèn)為一個(gè)單獨(dú)運(yùn)行起來的部分是一個(gè)進(jìn)程,因?yàn)樗彩且粋€(gè)數(shù)據(jù)集合構(gòu)成的運(yùn)行單位。只不過,前文說了我們一般認(rèn)為進(jìn)程和線程是包含關(guān)系,所以這里我們就不要對(duì)一些細(xì)節(jié)上咬文嚼字了。

一個(gè)進(jìn)程整體除了跑程序以外,它還相當(dāng)于管理線程的司令官。只不過我們?cè)缙诟緵]有學(xué)過多線程的概念,因此這個(gè)司令官無需管理多個(gè)線程,而只需要管理一個(gè)線程。而單獨(dú)的這個(gè)線程也不需要怎么管理,直接一句一句代碼調(diào)用就完事了,所以這個(gè)司令官在單線程程序上用途不大;而多線程的話,因?yàn)槎嗑€程的機(jī)制關(guān)系,司令官就需要去調(diào)整它們的運(yùn)行順序、關(guān)系,這就是多線程里調(diào)度(Dispatch)的概念。

我們先來介紹一下入門的內(nèi)容。為什么多線程需要調(diào)度?在底層,一個(gè)程序的多線程機(jī)制,并不是真正的多個(gè)線程全部并駕齊驅(qū)在運(yùn)行,而是一段時(shí)間內(nèi),線程 A 單獨(dú)執(zhí)行;等待時(shí)間到后,進(jìn)程會(huì)讓線程 A 的任務(wù)保留下來,暫停掉它,然后繼續(xù)線程 B 繼續(xù)執(zhí)行。假設(shè)我們這個(gè)程序只有 A 和 B 兩個(gè)線程的話,假設(shè)線程 A 先開始執(zhí)行的話,那么大概就是 A 執(zhí)行了一段時(shí)間后 B 繼續(xù)執(zhí)行;B 執(zhí)行一段時(shí)間后,又回到 A 繼續(xù)執(zhí)行,這么交替著來的。我們把運(yùn)行的這個(gè)時(shí)間段稱為一個(gè)時(shí)間片(Time Slice)。一般按常理來說,因?yàn)闀r(shí)間片很短,所以宏觀角度來看你是感受不到切換的,所以你完全可以認(rèn)為在宏觀意義上兩個(gè)線程在并行執(zhí)行,這就是多線程底層的執(zhí)行情況。順帶一提,切換線程這個(gè)行為我們稱為切換上下文(Context Switching)。

可能你會(huì)問,為什么不直接把多線程的模型設(shè)計(jì)成多個(gè)線程并駕齊驅(qū)的樣子,而不得不一個(gè)線程一個(gè)線程執(zhí)行,還換著來呢?這就需要介紹一個(gè)比較細(xì)節(jié)的地方了。線程的調(diào)度都發(fā)生在 CPU(中央處理器)里。這個(gè)部件是用來執(zhí)行和運(yùn)行程序的核心。按照理論來說,CPU 和內(nèi)核(物理或者虛擬 CPU)是專門提供程序運(yùn)行執(zhí)行的一個(gè)硬件。而問題在于,一個(gè) CPU 只可執(zhí)行運(yùn)行一個(gè)線程(倒過來說,一個(gè)線程可分配到一個(gè) CPU 的話,那么各自線程都有各自分配的 CPU,那么它們就可以獨(dú)立運(yùn)行);但問題就在于,整個(gè)操作系統(tǒng)運(yùn)行起來可是成百上千的進(jìn)程啊,而每一個(gè)進(jìn)程又都可以有一個(gè)或多個(gè)不同的線程。這樣這么多的線程要在你的電腦上跑起來,那么不得不需要每一個(gè)線程獨(dú)自占有一個(gè) CPU 才能達(dá)到理論上并行運(yùn)行的過程??蓡栴}就在于,一個(gè)電腦根本不可能做到這么多 CPU 的情況。即使像是因特爾公司的超線程技術(shù)(因特爾的超線程技術(shù)可以讓一個(gè) CPU 體現(xiàn)出多個(gè) CPU 執(zhí)行的效果),但這樣多的線程,再怎么超線程也達(dá)不到理論情況。因此,多線程的底層只能是采用上下文切換的方式。而超線程這類的技術(shù)也只能是配合這樣的技術(shù)來達(dá)到輔助效果。

另外,上下文切換、線程執(zhí)行等等步驟全部都是要耗費(fèi)執(zhí)行時(shí)間的。只是說切換上下文我們經(jīng)常在計(jì)算和運(yùn)行期間被我們忽略掉了,因?yàn)樗挠脮r(shí)非常短,但不代表切換上下文不用時(shí)間。

Part 2 數(shù)據(jù)操作的原子性

我們基本給大家介紹了底層的多線程調(diào)度的概念,在宏觀意義上它們是無法感知到切換,所以才認(rèn)為好像是并駕齊驅(qū)地在運(yùn)行程序;而在微觀概念上,它們并不是這樣。多線程的底層實(shí)際上也是“單線程+切換”這樣的實(shí)現(xiàn)模式。那么,在極端情況下,這樣的執(zhí)行過程會(huì)讓程序出現(xiàn)一些單線程遇不到的 bug。

假設(shè)我現(xiàn)在有一個(gè)程序包含線程 A 和 B。假設(shè) A 線程在執(zhí)行變量增大一個(gè)單位的時(shí)候(還沒執(zhí)行完)就切換時(shí)間片了,現(xiàn)在 B 就得開始執(zhí)行。

大概代碼是這樣的:

我先問一下看這份教程的你,a++ 是不是真的是一個(gè)執(zhí)行步驟?你可能會(huì)說,a++ 是一個(gè)語句嘛,那當(dāng)然是一個(gè)步驟了。但實(shí)際上,a++ 等價(jià)于 a = a + 1 這個(gè)執(zhí)行語句。而 a = a + 1 要做的操作有這些:

  • 把變量 a 的值取出來;

  • 把取出的數(shù)值和 1 加起來,得到的結(jié)果放在 = 運(yùn)算符左邊;

  • 把新的計(jì)算結(jié)果賦值給 a 變量。

因此,它能夠拆分為 3 個(gè)子步驟。那么我們來思考一個(gè)問題。假設(shè)我線程 A 還沒等到 a++ 這三個(gè)子步驟全部執(zhí)行完成,時(shí)間片就結(jié)束了的話,那么線程 A 此時(shí)的結(jié)果就會(huì)使得 B 的運(yùn)行效果不同。如果 a 初始值是 3 的話,那么線程 B 運(yùn)行起來的輸出結(jié)果就可能是 3 或者 4 兩種情況。3 是當(dāng)線程 A 沒有完成對(duì)變量 a 的賦值過程就已經(jīng)切換線程了,而 4 則是已經(jīng)完成了賦值,然后才執(zhí)行到 B 的。

這就是多線程執(zhí)行起來會(huì)產(chǎn)生的副作用:不可再現(xiàn)性。所謂的不可再現(xiàn)性你可以直觀地理解為這個(gè)程序只要給出相同初始值的話,執(zhí)行起來的結(jié)果是一致的,那么這個(gè)程序的結(jié)果就是可再現(xiàn)的;而多線程的機(jī)制會(huì)使得相同的初始值,結(jié)果也不一定一致,所以這種程序的結(jié)果就是不可再現(xiàn)的。

這個(gè)是剛才的程序。不過這么直接運(yùn)行你是看不到效果的,因?yàn)椤绦蛱炝?。程序?zhí)行一個(gè)步驟連你眨眼的時(shí)間都用不到,所以這樣的程序從運(yùn)行的角度來說你是很難捕捉到細(xì)節(jié)的。這里僅供參考。

如果一個(gè)步驟無法繼續(xù)拆分,那么我們就稱為這個(gè)步驟是原子(Atomic)的;換句話說,比如上面的 a++ 步驟,因?yàn)樗梢岳^續(xù)拆分為 3 個(gè)步驟,所以它不是原子的步驟。在計(jì)算機(jī)代碼的世界里,大多數(shù)過程都不是原子的。這也是在《C# 本質(zhì)論》一書上給大家說明清楚的一個(gè)點(diǎn):不要沒有根據(jù)地認(rèn)為普通代碼中原子的操作在多線程里也是原子的。而原子操作在運(yùn)行的時(shí)候只有兩種情況:要么還沒開始,要么已經(jīng)完成。比如 a++ 的操作就不算是原子操作,因?yàn)樵诙嗑€程里,a++ 可以執(zhí)行到中間(比如算出了和 1 計(jì)算求和結(jié)果,但還沒賦值過去),這也只能叫部分完成。

另外,多線程的角度下,由于大多數(shù)操作都并不是原子的,所以大多程序不一定有不可再現(xiàn)性。舉個(gè)例子,我們就把 a++ 和輸出代碼改成兩個(gè) for 循環(huán):

然后我們把程序直接運(yùn)行起來,你可以發(fā)現(xiàn),每一次運(yùn)行啟動(dòng)程序,輸出的結(jié)果都不同,甚至可以說基本上遇不到兩個(gè)完全一致的結(jié)果。多線程包含線程切換的操作,而 for 循環(huán)里的 i++ 非原子操作,所以兩個(gè)線程的操作都不可能是原子的,因此結(jié)果一定是不可再現(xiàn)的。

比如上面兩個(gè)線程同時(shí)運(yùn)行,其中一個(gè)結(jié)果是這樣的。

Part 3 lock 語句的基本概念

既然大多數(shù)過程都是非原子的,那么我們就必須有對(duì)抗這個(gè)的方法。C# 允許我們使用一個(gè)叫做 lock 的語句來完成。

lock 語句的格式是這樣的:

是的,長相和前面學(xué)的 usingfixed 也差不多,只是這里的 lock 里只寫變量或表達(dá)式,而不是一個(gè)變量定義的語句。lock 的功能是把代碼段整體看成一個(gè)“假的原子操作”,線程一旦進(jìn)入 lock 包括的這段代碼里,那么它們就必須執(zhí)行完畢后才可退出。而 lock 右側(cè)小括號(hào)里的變量,一般是一個(gè)我們獨(dú)立創(chuàng)建的靜態(tài)只讀字段。它一般都是引用類型,至于它的數(shù)值,我們一般不關(guān)心,只希望它不是 null 就行。

舉個(gè)例子。我仍然使用前文一個(gè) for 循環(huán)輸出加號(hào),而另一個(gè) for 循環(huán)輸出減號(hào)的多線程示例來介紹 lock 語句的使用。我們?cè)谡麄€(gè)操作類里加上一個(gè) object 類型的實(shí)例,它只需要 new 一下:

我們只需要把它設(shè)置為 private static readonly 的修飾符組合即可。private 是防止外部別的類型訪問它(當(dāng)然這個(gè)程序也沒有別的類型,一般嚴(yán)謹(jǐn)一點(diǎn)的話,是要寫這個(gè) private 的),而 static 是表示對(duì)象在靜態(tài)過程里也可使用,readonly 修飾符是表示這個(gè)對(duì)象在聲明后就不可改變其中的內(nèi)容。然后我們隨便給這個(gè)變量取個(gè)名字。這里我們?nèi)∶兞繛?SyncRoot,其中的單詞 sync 是 synchronous(同步的)的縮寫,而 root 是“根”的意思。這個(gè)“根”是之前我們學(xué)的 GC 內(nèi)存回收里說到的概念:我們把一個(gè) GC 可管轄的內(nèi)存對(duì)象稱為一個(gè)根。因?yàn)檫@里它是引用類型對(duì)象(object 類型的),所以我們知道,GC 是肯定會(huì)管轄引用類型的對(duì)象的,因此它也被稱為一個(gè)根。

接著,有了這個(gè)看起來毫無意義的 SyncRoot 后,我們開始更改多線程的代碼。我們現(xiàn)在只需要在兩個(gè) for 循環(huán)的外部增加一個(gè) lock 語句即可:

即只需要包裹一個(gè) lock (SyncRoot) { ... } 的代碼塊即可。這里需要注意的是,兩個(gè)不同的線程都得加上 lock (SyncRoot),其中一個(gè)加上也是不行的,因?yàn)閯偛耪f過,lock 語句限制的是線程在這段代碼里是原子的,但別的線程的執(zhí)行過程和這里限制的這個(gè) lock 語句所在線程是沒關(guān)系的。所以它并不會(huì)限制到別的線程的執(zhí)行行為。

程序改裝完成。我們現(xiàn)在來看看結(jié)果:

此時(shí)我們就可以發(fā)現(xiàn),所有的加號(hào)和減號(hào)都合并在一起輸出了。因?yàn)閮蓚€(gè)線程的執(zhí)行的底層都是原子的了,它必須先執(zhí)行完畢某個(gè)線程后,再來執(zhí)行另外一個(gè)線程的內(nèi)容。

然后你會(huì)問我一個(gè)問題。為什么先輸出的是加號(hào)的序列?按線程的執(zhí)行順序,加號(hào)不是第二個(gè)線程的執(zhí)行內(nèi)容嗎?為什么反而第二個(gè)線程(后執(zhí)行的線程)會(huì)先輸出呢?這個(gè)原因是出在線程調(diào)度上的。兩個(gè)線程很好理解:一個(gè)線程執(zhí)行一個(gè)時(shí)間片后另外一個(gè)線程執(zhí)行,執(zhí)行了一個(gè)時(shí)間片又回到原來的線程繼續(xù)執(zhí)行。但請(qǐng)注意的是,這個(gè)執(zhí)行的開始點(diǎn)并不是優(yōu)先發(fā)出 Start 方法的線程被優(yōu)先執(zhí)行。程序里有兩個(gè)線程在發(fā)出 Start 方法被得到啟動(dòng),但不代表我先 ProcA 的線程先發(fā)出 Start 就先執(zhí)行。因?yàn)?Start 發(fā)出后,也僅僅是變更線程狀態(tài),使得整個(gè)程序可以開始調(diào)度這個(gè)線程的執(zhí)行過程。但具體情況還得留給 CPU 自己來做。它可以隨機(jī)取出一個(gè)線程來作為起始點(diǎn)來調(diào)用,也可以按照線程的優(yōu)先級(jí)等等信息來選擇線程調(diào)用。所以,它并不是固定的結(jié)果。從這個(gè)角度來說,線程也具有不可再現(xiàn)性,因?yàn)槠鹗键c(diǎn)是不一定隨時(shí)隨地都一樣的。

這個(gè)例子里,恰好是第二個(gè)線程先得到了運(yùn)行,所以先輸出了加號(hào)。在某些時(shí)候,減號(hào)也可能是會(huì)優(yōu)先輸出的。這取決于你電腦的 CPU。比如這樣的結(jié)果:

這就是我重新嘗試運(yùn)行幾次程序后得到的先輸出減號(hào)的運(yùn)行結(jié)果。

而這里的 SyncRoot 是什么呢?你可以認(rèn)為是多線程需要的“錨”。換句話說,要想程序能夠體現(xiàn)出“讓非原子操作執(zhí)行起來更像是原子操作”這樣的行為,我們就不得不需要至少有一個(gè)監(jiān)控行為的工具來完成這個(gè)任務(wù)。如果沒有它的話,那么沒有這樣的東西的支持就無法監(jiān)控多線程的真正執(zhí)行效果,那不還是跟以前沒區(qū)別了的嗎?

我們需要一個(gè)記錄狀態(tài)的變量就行。那么這樣的變量只需要不是 null 還是一個(gè)引用類型就好。不是 null 很好解釋,因?yàn)?null 是沒有實(shí)例化的引用類型的默認(rèn)數(shù)值,所以這不還是沒用嗎?不過,“是引用類型”這一點(diǎn)就比較難解釋了,這個(gè)我們以后再來說明。

哦對(duì),順帶一說。因?yàn)橐妙愋途涂梢缘脑挘韵袷?typeof(T) 的寫法,或者是引用類型里的 this 也都可以參與 lock 表達(dá)式的使用:

或者是

這些都是可以的。只是……一些細(xì)節(jié)我們不得不需要在線程同步的內(nèi)容里給大家討論,這一點(diǎn)將在后面的內(nèi)容里給大家介紹到。

Part 4 再談 Abort 實(shí)例方法

最后,我們來說一下 Thread.Abort 方法。這個(gè)方法我們并不建議使用,除了之前一節(jié)的內(nèi)容給大家說的那些以外,還有 lock 語句的一點(diǎn)原因。

考慮一種情況。如果正在執(zhí)行 lock 語句里的代碼的話,突然被 Abort 方法給線程直接終止掉了,這會(huì)如何呢?Abort 方法在底層會(huì)產(chǎn)生一個(gè)叫做 ThreadAbortException 的異常類型實(shí)例,可問題就在于,這個(gè)異常拋出總是會(huì)終止程序的運(yùn)行。雖然這個(gè)異常我們可以使用 catch 捕獲,但也總有時(shí)候我們會(huì)忘記或者不需要捕獲這個(gè)異常的地方。在這種時(shí)候,因?yàn)楫惓]有捕獲,因此它會(huì)直接拋出;而與此同時(shí),因?yàn)槲覀冊(cè)趫?zhí)行 lock 語句,因此線程會(huì)被異常而立刻終止掉。這種情況下,Abort 方法是唯一能夠破壞 lock 的非原子轉(zhuǎn)原子的過程的方法。但這種破壞也會(huì)使得程序變得非常不穩(wěn)定和不安全。

我們把破壞原來代碼運(yùn)行過程的期望行為稱為線程安全性(Thread Safety)。注意線程安全性并不是線程的不可再現(xiàn)性的綁定概念。線程安全是說明線程使用過程期間是不是正常的、正確的、穩(wěn)定的、安全的,它無關(guān)執(zhí)行順序,只要程序沒有達(dá)到不穩(wěn)定的情況,就是線程安全的;而剛才這種 Abort 方法直接破壞了 lock 語句所規(guī)定和定義的執(zhí)行過程的行為,這就是破壞了線程的安全性,因此這種情況稱為線程不安全;反之就是線程安全。

所以按照上面介紹的文字來總結(jié)一下的話,Abort 方法會(huì)破壞線程安全性,因此這也是 Abort 方法的另外一個(gè)非常不建議使用的原因。


第 60 講:多線程(二):多線程的線程安全性的評(píng)論 (共 條)

分享到微博請(qǐng)遵守國家法律
亚东县| 通河县| 元朗区| 如东县| 色达县| 金塔县| 建水县| 泊头市| 射洪县| 丁青县| 江阴市| 乐至县| 梓潼县| 平顺县| 兴海县| 彝良县| 泰来县| 明星| 中超| 天柱县| 内江市| 汝阳县| 独山县| 东城区| 侯马市| 瑞昌市| 达孜县| 青浦区| 鸡西市| 安乡县| 黄龙县| 普兰县| 门头沟区| 灯塔市| 拜泉县| 康定县| 多伦县| 太保市| 洪雅县| 柯坪县| 昌宁县|