go并發(fā)編程入門到放棄
并發(fā)和并行
并發(fā):一個(gè)處理器同時(shí)處理多個(gè)任務(wù)。
并行:多個(gè)處理器或者是多核的處理器同時(shí)處理多個(gè)不同的任務(wù).
前者是邏輯上的同時(shí)發(fā)生(simultaneous),而后者是物理上的同時(shí)發(fā)生。并發(fā)是指同時(shí)有很多事要做,你可以串行處理也可以并行處理。并行是指同時(shí)做多件事。
并發(fā)性(concurrency),又稱共行性,是指能處理多個(gè)同時(shí)性活動(dòng)的能力,并發(fā)事件之間不一定要同一時(shí)刻發(fā)生。
并行(parallelism)是指同時(shí)發(fā)生的兩個(gè)并發(fā)事件,具有并發(fā)的含義,而并發(fā)則不一定并行。
如果某個(gè)系統(tǒng)支持兩個(gè)或者多個(gè)動(dòng)作(Action)同時(shí)存在,那么這個(gè)系統(tǒng)就是一個(gè)并發(fā)系統(tǒng)。如果某個(gè)系統(tǒng)支持兩個(gè)或者多個(gè)動(dòng)作同時(shí)執(zhí)行,那么這個(gè)系統(tǒng)就是一個(gè)并行系統(tǒng)。并發(fā)系統(tǒng)與并行系統(tǒng)這兩個(gè)定義之間的關(guān)鍵差異在于“存在”這個(gè)詞。
在并發(fā)程序中可以同時(shí)擁有兩個(gè)或者多個(gè)線程。這意味著,如果程序在單核處理器上運(yùn)行,那么這兩個(gè)線程將交替地?fù)Q入或者換出內(nèi)存。這些線程是同時(shí)“存在”的——每個(gè)線程都處于執(zhí)行過(guò)程中的某個(gè)狀態(tài)。如果程序能夠并行執(zhí)行,那么就一定是運(yùn)行在多核處理器上。此時(shí),程序中的每個(gè)線程都將分配到一個(gè)獨(dú)立的處理器核上,因此可以同時(shí)運(yùn)行。
我相信你已經(jīng)能夠得出結(jié)論——“并行”概念是“并發(fā)”概念的一個(gè)子集。也就是說(shuō),你可以編寫一個(gè)擁有多個(gè)線程或者進(jìn)程的并發(fā)程序,但如果沒(méi)有多核處理器來(lái)執(zhí)行這個(gè)程序,那么就不能以并行方式來(lái)運(yùn)行代碼。因此,凡是在求解單個(gè)問(wèn)題時(shí)涉及多個(gè)執(zhí)行流程的編程模式或者執(zhí)行行為,都屬于并發(fā)編程的范疇。
線程可以并發(fā)運(yùn)行(每個(gè)線程在單個(gè)內(nèi)核上輪流運(yùn)行),也可以并行運(yùn)行(每個(gè)線程在不同的內(nèi)核上同時(shí)運(yùn)行)。
進(jìn)程、線程、協(xié)程和goroutine
Process -> Thread(LWP, lightweight process) -> Goroutine (一種lightweight userspace thread)
進(jìn)程(process)
狹義定義:進(jìn)程就是一段程序的執(zhí)行過(guò)程例如啟動(dòng)的某個(gè)app。
廣義定義:進(jìn)程是一個(gè)具有獨(dú)立功能的程序關(guān)于某個(gè)數(shù)據(jù)集合的一次運(yùn)行活動(dòng)。它是操作系統(tǒng)動(dòng)態(tài)執(zhí)行的基本單元,在傳統(tǒng)的操作系統(tǒng)中,進(jìn)程即是基本的分配單元,也是基本的執(zhí)行單元。
進(jìn)程是一個(gè)實(shí)體,每個(gè)進(jìn)程都有自己的地址空間,一般情況下,包含文本區(qū)域、數(shù)據(jù)區(qū)域、堆棧
進(jìn)程是執(zhí)行中的程序,程序是一個(gè)沒(méi)有生命的實(shí)體,只有處理器賦予程序生命時(shí),它才能成為一個(gè)活動(dòng)的實(shí)體,我們稱之為進(jìn)程
進(jìn)程本身不會(huì)運(yùn)行,是線程的容器。線程不能單獨(dú)執(zhí)行,必須組成進(jìn)程
一個(gè)程序至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程至少有一個(gè)線程
對(duì)于操作系統(tǒng)來(lái)講,一個(gè)任務(wù)就是一個(gè)進(jìn)程,比如開(kāi)一個(gè)瀏覽器就是啟動(dòng)一個(gè)瀏覽器進(jìn)程。打開(kāi)一款app就是打開(kāi)一個(gè)進(jìn)程,例如打開(kāi)香哈就是運(yùn)行了一個(gè)進(jìn)程。
有些進(jìn)程還不止同時(shí)做一件事情。比如打開(kāi)香哈,它可以同時(shí)進(jìn)行看視頻并且回復(fù)用戶評(píng)論,在一個(gè)進(jìn)程內(nèi)部,要同時(shí)干多件事情。
進(jìn)程狀態(tài)
三狀態(tài)模型
就緒:獲取出CPU外的所有資源、只要處理器分配資源就可以馬上執(zhí)行
運(yùn)行:獲得處理器分配的資源,程序開(kāi)始執(zhí)行
阻塞:當(dāng)程序條件不夠的時(shí)候,需要等待提交滿足的時(shí)候才能執(zhí)行。
五狀態(tài)模型
創(chuàng)建狀態(tài):進(jìn)程在創(chuàng)建時(shí)需要申請(qǐng)一個(gè)空白PCB,向其中填寫控制和管理進(jìn)程的信息,完成資源分配。如果創(chuàng)建工作無(wú)法完成,比如資源無(wú)法滿足,就無(wú)法被調(diào)度運(yùn)行,把此時(shí)進(jìn)程所處狀態(tài)稱為創(chuàng)建狀態(tài)
就緒狀態(tài):進(jìn)程已經(jīng)準(zhǔn)備好,已分配到所需資源,只要分配到CPU就能夠立即運(yùn)行
執(zhí)行狀態(tài):進(jìn)程處于就緒狀態(tài)被調(diào)度后,進(jìn)程進(jìn)入執(zhí)行狀態(tài)
阻塞狀態(tài):正在執(zhí)行的進(jìn)程由于某些事件(I/O請(qǐng)求,申請(qǐng)緩存區(qū)失敗)而暫時(shí)無(wú)法運(yùn)行,進(jìn)程受到阻塞。在滿足請(qǐng)求時(shí)進(jìn)入就緒狀態(tài)等待系統(tǒng)調(diào)用
終止?fàn)顟B(tài):進(jìn)程結(jié)束,或出現(xiàn)錯(cuò)誤,或被系統(tǒng)終止,進(jìn)入終止?fàn)顟B(tài)。無(wú)法再執(zhí)行
最早的并發(fā)能力:多進(jìn)程并發(fā),當(dāng)一個(gè)進(jìn)程阻塞的時(shí)候,切換到另外等待執(zhí)行的進(jìn)程,這樣就能盡量把CPU利用起來(lái),CPU就不浪費(fèi)了。
線程(thread)
線程是一個(gè)程序里面不同的執(zhí)行路徑。
為什么引入線程
應(yīng)用的需要。比如打開(kāi)一個(gè)瀏覽器,我想一邊瀏覽網(wǎng)頁(yè),一邊下載文件,一邊播放音樂(lè)。如果一個(gè)瀏覽器是一個(gè)進(jìn)程,那么這樣的需求需要線程機(jī)制。
開(kāi)銷的考慮。在進(jìn)程內(nèi)創(chuàng)建、終止線程比創(chuàng)建、終止進(jìn)程要快。同一進(jìn)程內(nèi)的線程間切換比進(jìn)程間的切換要快,尤其是用戶級(jí)線程間的切換。線程之間相互通信無(wú)須通過(guò)內(nèi)核(同一進(jìn)程內(nèi)的線程共享內(nèi)存和文件)
性能的考慮。多個(gè)線程中,任務(wù)功能不同(有的負(fù)責(zé)計(jì)算,有的負(fù)責(zé)I/O),如果有多個(gè)處理器,一個(gè)進(jìn)程就可以有很多的任務(wù)同時(shí)在執(zhí)行。
線程狀態(tài):
- 初始狀態(tài)
實(shí)現(xiàn)Runnable接口和繼承Thread可以得到一個(gè)線程類,new一個(gè)實(shí)例出來(lái),線程就進(jìn)入了初始狀態(tài)。
- 就緒狀態(tài)
就緒狀態(tài)只是說(shuō)你資格運(yùn)行,調(diào)度程序沒(méi)有挑選到你,你就永遠(yuǎn)是就緒狀態(tài)。調(diào)用線程的start()方法,此線程進(jìn)入就緒狀態(tài)。當(dāng)前線程sleep()方法結(jié)束,其他線程join()結(jié)束,等待用戶輸入完畢,某個(gè)線程拿到對(duì)象鎖,這些線程也將進(jìn)入就緒狀態(tài)。當(dāng)前線程時(shí)間片用完了,調(diào)用當(dāng)前線程的yield()方法,當(dāng)前線程進(jìn)入就緒狀態(tài)。鎖池里的線程拿到對(duì)象鎖后,進(jìn)入就緒狀態(tài)。
- 運(yùn)行中狀態(tài)
線程調(diào)度程序從可運(yùn)行池中選擇一個(gè)線程作為當(dāng)前線程時(shí)線程所處的狀態(tài)。這也是線程進(jìn)入運(yùn)行狀態(tài)的唯一一種方式。
- 阻塞狀態(tài)
阻塞狀態(tài)是線程阻塞在進(jìn)入synchronized關(guān)鍵字修飾的方法或代碼塊(獲取鎖)時(shí)的狀態(tài)。
- 等待狀態(tài)
處于這種狀態(tài)的線程不會(huì)被分配CPU執(zhí)行時(shí)間,它們要等待被顯式地喚醒,否則會(huì)處于無(wú)限期等待的狀態(tài)。超時(shí)等待處于這種狀態(tài)的線程不會(huì)被分配CPU執(zhí)行時(shí)間,不過(guò)無(wú)須無(wú)限期等待被其他線程顯示地喚醒,在達(dá)到一定時(shí)間后它們會(huì)自動(dòng)喚醒。
- 終止?fàn)顟B(tài)
當(dāng)線程的run()方法完成時(shí),或者主線程的main()方法完成時(shí),我們就認(rèn)為它終止了。這個(gè)線程對(duì)象也許是活的,但是,它已經(jīng)不是一個(gè)單獨(dú)執(zhí)行的線程。線程一旦終止了,就不能復(fù)生。在一個(gè)終止的線程上調(diào)用start()方法,會(huì)拋出java.lang.IllegalThreadStateException異常。
java線程狀態(tài)
用戶級(jí)線程和內(nèi)核級(jí)線程
用戶態(tài)線程屬于自己用戶態(tài)創(chuàng)建、管理和銷毀,運(yùn)行模式也在用戶態(tài)ring3
內(nèi)核態(tài)則運(yùn)行與內(nèi)核中,由內(nèi)核調(diào)度
內(nèi)核線程由操作系統(tǒng)調(diào)度
用戶線程由系統(tǒng) 或者基于系統(tǒng)的第三方支持庫(kù)完成,如 pthread,c11提到但并沒(méi)實(shí)現(xiàn)的輕量級(jí)多線程庫(kù)等等.
// TODO Fxxk
進(jìn)程和線程
進(jìn)程是一個(gè)資源的容器,為進(jìn)程里的所有線程提供共享資源,是對(duì)程序的一種靜態(tài)描述;線程是計(jì)算機(jī)最小的調(diào)度和運(yùn)行單位,是對(duì)程序的一種動(dòng)態(tài)描述
開(kāi)個(gè)QQ,開(kāi)了一個(gè)進(jìn)程;開(kāi)了迅雷,開(kāi)了一個(gè)進(jìn)程。在QQ的這個(gè)進(jìn)程里,傳輸文字開(kāi)一個(gè)線程、傳輸語(yǔ)音開(kāi)了一個(gè)線程、彈出對(duì)話框又開(kāi)了一個(gè)線程。
進(jìn)程就是程序在操作系統(tǒng)中的依次執(zhí)行過(guò)程,是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位。
線程是進(jìn)程的一個(gè)執(zhí)行實(shí)例,是程序執(zhí)行的最小單元,它是比進(jìn)程更小的能獨(dú)立運(yùn)行的基本單位
一個(gè)進(jìn)程可以創(chuàng)建和銷毀多個(gè)線程,同時(shí)一個(gè)進(jìn)程中的多個(gè)線程可以并發(fā)執(zhí)行
一個(gè)程序至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程至少有一個(gè)線程
區(qū)別
定義方面:進(jìn)程是程序在某個(gè)數(shù)據(jù)集合上的一次運(yùn)行活動(dòng);線程是進(jìn)程中的一個(gè)執(zhí)行路徑。(進(jìn)程可以創(chuàng)建多個(gè)線程)
角色方面:在支持線程機(jī)制的系統(tǒng)中,進(jìn)程是系統(tǒng)資源分配的單位,線程是CPU調(diào)度的單位。
資源共享方面:進(jìn)程之間不能共享資源,而線程共享所在進(jìn)程的地址空間和其它資源。同時(shí)線程還有自己的棧和棧指針,程序計(jì)數(shù)器等寄存器。
獨(dú)立性方面:進(jìn)程有自己獨(dú)立的地址空間,而線程沒(méi)有,線程必須依賴于進(jìn)程而存在。
開(kāi)銷方面。進(jìn)程切換的開(kāi)銷較大。線程相對(duì)較小。(前面也提到過(guò),引入線程也出于了開(kāi)銷的考慮。)
協(xié)程(coroutine)
協(xié)程,是在應(yīng)用層模擬的線程,他避免了上下文切換的額外耗費(fèi),兼顧了多線程的優(yōu)點(diǎn)。簡(jiǎn)化了高并發(fā)程序的復(fù)雜度。舉個(gè)例子,一個(gè)高并發(fā)的網(wǎng)絡(luò)服務(wù)器,每一個(gè)socket連接進(jìn)來(lái),服務(wù)器用一個(gè)協(xié)程來(lái)對(duì)他進(jìn)行服務(wù)。代碼非常清晰。而且兼顧了性能。
協(xié)程是與其他函數(shù)或方法一起并發(fā)運(yùn)行的函數(shù)或方法。協(xié)程可以看作是輕量級(jí)線程。與線程相比,創(chuàng)建一個(gè)協(xié)程的成本很小。因此在協(xié)程應(yīng)用中,常常會(huì)看到有數(shù)以千計(jì)的協(xié)程并發(fā)地運(yùn)行。
線程和協(xié)程
對(duì)于 進(jìn)程、線程,都是有內(nèi)核進(jìn)行調(diào)度,有 CPU 時(shí)間片的概念,進(jìn)行 搶占式調(diào)度(有多種調(diào)度算法)。
對(duì)于 協(xié)程(用戶級(jí)線程),這是對(duì)內(nèi)核透明的,也就是系統(tǒng)并不知道有協(xié)程的存在,是完全由用戶自己的程序進(jìn)行調(diào)度的,因?yàn)槭怯捎脩舫绦蜃约嚎刂?,那么就很難像搶占式調(diào)度那樣做到強(qiáng)制的 CPU 控制權(quán)切換到其他進(jìn)程/線程,通常只能進(jìn)行 協(xié)作式調(diào)度,需要協(xié)程自己主動(dòng)把控制權(quán)轉(zhuǎn)讓出去之后,其他協(xié)程才能被執(zhí)行到。
協(xié)程需要運(yùn)行在線程之上,線程由CPU進(jìn)行調(diào)度。
<colgroup><col width="243"><col width="262"><col width="234"></colgroup>
|
比較的點(diǎn)
|
線程
|
協(xié)程
|
|
數(shù)據(jù)存儲(chǔ)
|
內(nèi)核態(tài)的內(nèi)存空間
|
一般是線程提供的用戶態(tài)內(nèi)存空間
|
|
切換操作
|
操作最終在內(nèi)核層完成,應(yīng)用層需要調(diào)用內(nèi)核層提供的 syscall 底層函數(shù)
|
應(yīng)用層使用代碼進(jìn)行簡(jiǎn)單的現(xiàn)場(chǎng)保存和恢復(fù)即可
|
|
切換開(kāi)銷
|
涉及模式切換(從用戶態(tài)切換到內(nèi)核態(tài))、16個(gè)寄存器、PC、SP...等寄存器的刷新等
|
goroutine:只有三個(gè)寄存器的值修改 - PC / SP / DX
|
|
任務(wù)調(diào)度
|
由內(nèi)核實(shí)現(xiàn),搶占方式,依賴各種鎖
|
由用戶態(tài)的實(shí)現(xiàn)的具體調(diào)度器進(jìn)行。例如 go 協(xié)程的調(diào)度器
|
|
語(yǔ)音支持程度
|
絕大部分編程語(yǔ)言
|
部分語(yǔ)言:Lua,Go,Python ...
|
|
實(shí)現(xiàn)規(guī)范
|
按照現(xiàn)代操作系統(tǒng)規(guī)范實(shí)現(xiàn) 無(wú)統(tǒng)一規(guī)范。
|
在應(yīng)用層由開(kāi)發(fā)者實(shí)現(xiàn),高度自定義,比如只支持單線程的線程。不同的調(diào)度策略,等等
|
|
內(nèi)存消耗方面
|
線程:8MB
|
goroutine:2KB
|
協(xié)程和線程有3種映射關(guān)系:
N:1,N個(gè)協(xié)程綁定1個(gè)線程,優(yōu)點(diǎn)就是協(xié)程在用戶態(tài)線程即完成切換,不會(huì)陷入到內(nèi)核態(tài),這種切換非常的輕量快速。但也有很大的缺點(diǎn),1個(gè)進(jìn)程的所有協(xié)程都綁定在1個(gè)線程上,一是某個(gè)程序用不了硬件的多核加速能力,二是一旦某協(xié)程阻塞,造成線程阻塞,本進(jìn)程的其他協(xié)程都無(wú)法執(zhí)行了,根本就沒(méi)有并發(fā)的能力了。
1:1,1個(gè)協(xié)程綁定1個(gè)線程,這種最容易實(shí)現(xiàn)。協(xié)程的調(diào)度都由CPU完成了,不存在N:1缺點(diǎn),但有一個(gè)缺點(diǎn)是協(xié)程的創(chuàng)建、刪除和切換的代價(jià)都由CPU完成,有點(diǎn)略顯昂貴了。
M:N,M個(gè)協(xié)程綁定1個(gè)線程,是N:1和1:1類型的結(jié)合,克服了以上2種模型的缺點(diǎn),但實(shí)現(xiàn)起來(lái)最為復(fù)雜。
goroutine
Go中,協(xié)程被稱為goroutine(Rob Pike說(shuō)goroutine不是協(xié)程,因?yàn)樗麄儾⒉煌耆嗤浅]p量,一個(gè)goroutine只占幾KB,并且這幾KB就足夠goroutine運(yùn)行完,這就能在有限的內(nèi)存空間內(nèi)支持大量goroutine,支持了更多的并發(fā)。雖然一個(gè)goroutine的棧只占幾KB,但實(shí)際是可伸縮的,如果需要更多內(nèi)容,?runtime?會(huì)自動(dòng)為goroutine分配。
goroutine實(shí)現(xiàn)了M:N協(xié)程和線程的映射關(guān)系。
協(xié)程和goroutine
本質(zhì)上,goroutine 就是協(xié)程。 不同的是,Golang 在 runtime、系統(tǒng)調(diào)用等多方面對(duì) goroutine 調(diào)度進(jìn)行了封裝和處理,當(dāng)遇到長(zhǎng)時(shí)間執(zhí)行或者進(jìn)行系統(tǒng)調(diào)用時(shí),會(huì)主動(dòng)把當(dāng)前 goroutine 的CPU (P) 轉(zhuǎn)讓出去,讓其他 goroutine 能被調(diào)度并執(zhí)行,也就是 Golang 從語(yǔ)言層面支持了協(xié)程。Golang 的一大特色就是從語(yǔ)言層面原生支持協(xié)程,在函數(shù)或者方法前面加 go關(guān)鍵字就可創(chuàng)建一個(gè)協(xié)程。
Go 調(diào)度器
內(nèi)核調(diào)度,協(xié)程調(diào)度,進(jìn)程的創(chuàng)建銷毀都有內(nèi)核調(diào)度完成,goroutine的創(chuàng)建運(yùn)行銷毀有GO的scheduler完成。
調(diào)度器的任務(wù)是在用戶態(tài)完成goroutine的調(diào)度,而調(diào)度器的實(shí)現(xiàn)好壞,對(duì)并發(fā)實(shí)際有很大的影響,并且Go的調(diào)度器就是M:N類型的,實(shí)現(xiàn)起來(lái)也是最復(fù)雜。
Why
熟悉POSIX API的人都知道,POSIX的方案在很大程度上是對(duì)Unix process進(jìn)場(chǎng)模型的一個(gè)邏輯描述和擴(kuò)展,兩者有很多相似的地方。 Thread有自己的信號(hào)掩碼,CPU affinity等。但是很多特征對(duì)于Go程序來(lái)說(shuō)都是累贅。 尤其是context上下文切換的耗時(shí)。另一個(gè)原因是Go的垃圾回收需要所有的goroutine停止,使得內(nèi)存在一個(gè)一致的狀態(tài)。垃圾回收的時(shí)間點(diǎn)是不確定的,如果依靠OS自身的scheduler來(lái)調(diào)度,那么會(huì)有大量的線程需要停止工作。
簡(jiǎn)而言之,就是基礎(chǔ)架構(gòu)做了個(gè)東西我們用起來(lái)不方便,在上面包裝做了個(gè)新東西,比如我們的決策平臺(tái)在dolphin上包裝了一層。
Old Go Scheduler
2012年以前,go采用MG模型實(shí)現(xiàn)協(xié)程調(diào)度。
M,代表線程,它要運(yùn)行g(shù)oroutine。
Global G Queue,是全局goroutine隊(duì)列,所有的goroutine都保存在這個(gè)隊(duì)列中,goroutine用G進(jìn)行代表。
M想要執(zhí)行、放回G都必須訪問(wèn)全局G隊(duì)列,并且M有多個(gè),即多線程訪問(wèn)同一資源需要加鎖進(jìn)行保證互斥/同步,所以全局G隊(duì)列是有互斥鎖進(jìn)行保護(hù)的。
老調(diào)度器有4個(gè)缺點(diǎn):
創(chuàng)建、銷毀、調(diào)度G都需要每個(gè)M獲取鎖,這就形成了激烈的鎖競(jìng)爭(zhēng)。
M轉(zhuǎn)移G會(huì)造成延遲和額外的系統(tǒng)負(fù)載。比如當(dāng)G中包含創(chuàng)建新協(xié)程的時(shí)候,M創(chuàng)建了G’,為了繼續(xù)執(zhí)行G,需要把G’交給M’執(zhí)行,也造成了很差的局部性,因?yàn)镚’和G是相關(guān)的,最好放在M上執(zhí)行,而不是其他M'。
M中的mcache是用來(lái)存放小對(duì)象的,mcache和棧都和M關(guān)聯(lián)造成了大量的內(nèi)存開(kāi)銷和差的局部性。
系統(tǒng)調(diào)用導(dǎo)致頻繁的線程阻塞和取消阻塞操作增加了系統(tǒng)開(kāi)銷。
New Go Scheduler
面對(duì)以上老調(diào)度的問(wèn)題,Go設(shè)計(jì)了新的調(diào)度器,設(shè)計(jì)文稿:https://golang.org/s/go11sched
新調(diào)度器引入了:
P:Processor,它包含了運(yùn)行g(shù)oroutine的資源,如果線程想運(yùn)行g(shù)oroutine,必須先獲取P,P中還包含了可運(yùn)行的G隊(duì)列。
work stealing:當(dāng)M綁定的P沒(méi)有可運(yùn)行的G時(shí),它可以從其他運(yùn)行的M’那里偷取G。
現(xiàn)在,調(diào)度器中3個(gè)重要的縮寫你都接觸到了,所有文章都用這幾個(gè)縮寫,請(qǐng)牢記:
G: goroutine
M: 工作線程
P: 處理器,它包含了運(yùn)行Go代碼的資源,M必須和一個(gè)P關(guān)聯(lián)才能運(yùn)行G。
調(diào)度器的有兩大思想:
復(fù)用線程:協(xié)程本身就是運(yùn)行在一組線程之上,不需要頻繁的創(chuàng)建、銷毀線程,而是對(duì)線程的復(fù)用。在調(diào)度器中復(fù)用線程還有2個(gè)體現(xiàn):1)work stealing,當(dāng)本線程無(wú)可運(yùn)行的G時(shí),嘗試從其他線程綁定的P偷取G,而不是銷毀線程。2)hand off,當(dāng)本線程因?yàn)镚進(jìn)行系統(tǒng)調(diào)用阻塞時(shí),線程釋放綁定的P,把P轉(zhuǎn)移給其他空閑的線程執(zhí)行。
利用并行:GOMAXPROCS設(shè)置P的數(shù)量,當(dāng)GOMAXPROCS大于1時(shí),就最多有GOMAXPROCS個(gè)線程處于運(yùn)行狀態(tài),這些線程可能分布在多個(gè)CPU核上同時(shí)運(yùn)行,使得并發(fā)利用并行。另外,GOMAXPROCS也限制了并發(fā)的程度,比如GOMAXPROCS = 核數(shù)/2,則最多利用了一半的CPU核進(jìn)行并行。
調(diào)度器的兩小策略:
搶占:在coroutine中要等待一個(gè)協(xié)程主動(dòng)讓出CPU才執(zhí)行下一個(gè)協(xié)程,在Go中,一個(gè)goroutine最多占用CPU 10ms,防止其他goroutine被餓死,這就是goroutine不同于coroutine的一個(gè)地方。
全局G隊(duì)列:在新的調(diào)度器中依然有全局G隊(duì)列,但功能已經(jīng)被弱化了,當(dāng)M執(zhí)行work stealing從其他P偷不到G時(shí),它可以從全局G隊(duì)列獲取G。
上面提到并行了,關(guān)于并發(fā)和并行再說(shuō)一下:Go創(chuàng)始人Rob Pike一直在強(qiáng)調(diào)go是并發(fā),不是并行,因?yàn)镚o做的是在一段時(shí)間內(nèi)完成幾十萬(wàn)、甚至幾百萬(wàn)的工作,而不是同一時(shí)間同時(shí)在做大量的工作。并發(fā)可以利用并行提高效率,調(diào)度器是有并行設(shè)計(jì)的。
P M G
在scheduler中有三個(gè)非常重要的概念:P,M,G。
// Goroutine scheduler
// The scheduler's job is to distribute ready-to-run goroutines over worker threads.
//
// The main concepts are:
// G - goroutine.
// M - worker thread, or machine.
// P - processor, a resource that is required to execute Go code.
// M must have an associated P to execute Go code, however it can be
// blocked or in a syscall w/o an associated P.
//
// Design doc at https://golang.org/s/go11sched.
G: goroutine,運(yùn)行在M之上
M: 工作線程,有內(nèi)核調(diào)度
P: 處理器,它包含了運(yùn)行Go代碼的資源,M必須和一個(gè)P關(guān)聯(lián)才能運(yùn)行G。
自頂向下是調(diào)度器的4個(gè)部分:
全局隊(duì)列(Global Queue):存放等待運(yùn)行的G。
P的本地隊(duì)列:同全局隊(duì)列類似,存放的也是等待運(yùn)行的G,存的數(shù)量有限,不超過(guò)256個(gè)。新建G'時(shí),G'優(yōu)先加入到P的本地隊(duì)列,如果隊(duì)列滿了,則會(huì)把本地隊(duì)列中一半的G移動(dòng)到全局隊(duì)列。
P列表:所有的P都在程序啟動(dòng)時(shí)創(chuàng)建,并保存在數(shù)組中,最多有GOMAXPROCS個(gè)。
M:線程想運(yùn)行任務(wù)就得獲取P,從P的本地隊(duì)列獲取G,P隊(duì)列為空時(shí),M也會(huì)嘗試從全局隊(duì)列拿一批G放到P的本地隊(duì)列,或從其他P的本地隊(duì)列偷一半放到自己P的本地隊(duì)列。M運(yùn)行G,G執(zhí)行之后,M會(huì)從P獲取下一個(gè)G,不斷重復(fù)下去。
work stealing
調(diào)度流程
并發(fā)編程模型
工作類型
線程可以做兩種類型的工作。第一個(gè)稱為 CPU-Bound,第二個(gè)稱為 IO-Bound。
CPU-Bound:這種工作類型永遠(yuǎn)也不會(huì)讓線程處在等待狀態(tài),因?yàn)檫@是一項(xiàng)不斷進(jìn)行計(jì)算的工作。比如計(jì)算 π 的第 n 位,就是一個(gè) CPU-Bound 線程。
IO-Bound:這是導(dǎo)致線程進(jìn)入等待狀態(tài)的工作類型。比如通過(guò)網(wǎng)絡(luò)請(qǐng)求對(duì)資源的訪問(wèn)或?qū)Σ僮飨到y(tǒng)進(jìn)行系統(tǒng)調(diào)用。
七天七并發(fā)模型
線程與鎖:線程與鎖模型有很多眾所周知的不足,但仍是其他模型的技術(shù)基礎(chǔ),也是很多并發(fā)軟件開(kāi)發(fā)的首選。
函數(shù)式編程:函數(shù)式編程日漸重要的原因之一,是其對(duì)并發(fā)編程和并行編程提供了良好的支持。函數(shù)式編程消除了可變狀態(tài),所以從根本上是線程安全的,而且易于并行執(zhí)行。
Clojure之道——分離標(biāo)識(shí)與狀態(tài):編程語(yǔ)言Clojure是一種指令式編程和函數(shù)式編程的混搭方案,在兩種編程方式上取得了微妙的平衡來(lái)發(fā)揮兩者的優(yōu)勢(shì)。
actor:actor模型是一種適用性很廣的并發(fā)編程模型,適用于共享內(nèi)存模型和分布式內(nèi)存模型,也適合解決地理分布型問(wèn)題,能提供強(qiáng)大的容錯(cuò)性。
通信順序進(jìn)程(Communicating Sequential Processes,CSP):表面上看,CSP模型與actor模型很相似,兩者都基于消息傳遞。不過(guò)CSP模型側(cè)重于傳遞信息的通道,而actor模型側(cè)重于通道兩端的實(shí)體,使用CSP模型的代碼會(huì)帶有明顯不同的風(fēng)格。
數(shù)據(jù)級(jí)并行:每個(gè)筆記本電腦里都藏著一臺(tái)超級(jí)計(jì)算機(jī)——GPU。GPU利用了數(shù)據(jù)級(jí)并行,不僅可以快速進(jìn)行圖像處理,也可以用于更廣闊的領(lǐng)域。如果要進(jìn)行有限元分析、流體力學(xué)計(jì)算或其他的大量數(shù)字計(jì)算,GPU的性能將是不二選擇。
Lambda架構(gòu):大數(shù)據(jù)時(shí)代的到來(lái)離不開(kāi)并行——現(xiàn)在我們只需要增加計(jì)算資源,就能具有處理TB級(jí)數(shù)據(jù)的能力。Lambda架構(gòu)綜合了MapReduce和流式處理的特點(diǎn),是一種可以處理多種大數(shù)據(jù)問(wèn)題的架構(gòu)。
CSP和ACTOR
“Don’t communicate by sharing memory, share memory by communicating”(不要通過(guò)共享內(nèi)存來(lái)通信,而應(yīng)該通過(guò)通信來(lái)共享內(nèi)存)
GO CSP
Go語(yǔ)言的誕生就是為了支持高并發(fā),有2個(gè)支持高并發(fā)的模型:CSP和Actor。鑒于Occam和Erlang都選用了CSP(來(lái)自Go FAQ),并且效果不錯(cuò),Go也選了CSP,但與前兩者不同的是,Go把channel作為頭等公民。
就像前面說(shuō)的多線程編程太不友好了,Go為了提供更容易使用的并發(fā)方法,使用了goroutine和channel。goroutine來(lái)自協(xié)程的概念,讓一組可復(fù)用的函數(shù)運(yùn)行在一組線程之上,即使有協(xié)程阻塞,該線程的其他協(xié)程也可以被?runtime?調(diào)度,轉(zhuǎn)移到其他可運(yùn)行的線程上。最關(guān)鍵的是,程序員看不到這些底層的細(xì)節(jié),這就降低了編程的難度,提供了更容易的并發(fā)。
Goroutine和Channel
Channel
channel容易再犯錯(cuò),甚至于比使用傳統(tǒng)sync包下的同步原語(yǔ)的錯(cuò)誤率還要高,牢記異常的情況:
close已經(jīng)close的channel也會(huì)panic。
利用channel可以實(shí)現(xiàn)鎖,并且可以實(shí)現(xiàn)TryWithTimeout方法,因?yàn)槔肎o的內(nèi)存模型可以保障,但是正常情況channel和mutex有不同的應(yīng)用場(chǎng)景。
Channel
傳遞數(shù)據(jù)的owner
分發(fā)任務(wù)
交流異步結(jié)果
任務(wù)編排
Mutex
cache
狀態(tài)
臨界區(qū)
Channel的一些應(yīng)用模式可以參考另一篇文章: Go Channel 應(yīng)用模式
Go內(nèi)存模型
內(nèi)存模型描述了線程(goroutine)通過(guò)內(nèi)存的交互,以及對(duì)數(shù)據(jù)的共享使用。
Java語(yǔ)言是第一個(gè)詳細(xì)描述其內(nèi)存模型的流行的編程語(yǔ)言。
它并不是描述內(nèi)存是如何分配的,而是定義了:
對(duì)同一個(gè)變量,如何保證在一個(gè)goroutine對(duì)此變量讀的時(shí)候,能觀察到其它goroutine對(duì)此變量的寫。
描述這種順序關(guān)系的術(shù)語(yǔ)叫做happen before。
單個(gè)goroutine內(nèi)
執(zhí)行順序和代碼編寫的順序是一致的(有reorder,也不影響理解,可以按照編寫順序進(jìn)行分析)
包級(jí)別的init函數(shù)
在單個(gè)goroutine中執(zhí)行
最底層引入的包的init先執(zhí)行。之后再是main函數(shù)。
提供問(wèn)題: 同一個(gè)包下可以定義多個(gè)init函數(shù)嗎?
go語(yǔ)句
goroutine的創(chuàng)建happens before所有此goroutine中的操作
goroutine的銷毀happens after所有此goroutine中的操作
channel
第n個(gè)send一定happen before第n個(gè)receive完成, 不管是buffered channel還是unbuffered channel
對(duì)于capacity 為m的channel,第n個(gè)receive一定happen before第 (n+m) send完成
m=0 unbuffered。第n個(gè)receive一定happen before第n個(gè)send完成
channel的close一定happen before receive端得到通知,得到通知意味著receive收到一個(gè)因?yàn)閏hannel close而收到的零值
注意 send/send completes,receive/receive completes的區(qū)別
Mutex/****RWMutex
對(duì)于Mutex/RWMutx m, 第n個(gè)成功的 m.Unlock 一定happen before 第 n+1 m.Lock方法調(diào)用的返回
對(duì)于RWMutex rw,如果它的第n個(gè)rw.Lock已返回,那么它的第n個(gè)成功的rw.Unlock的方法調(diào)用一定happen before 任何一個(gè) rw.RLock方法調(diào)用的返回(它們 happen after 第n個(gè)rw.Lock方法調(diào)用返回)
對(duì)于RWMutex rw,如果它的第n個(gè)rw.RLock已返回,接著第m (m < n)個(gè)rm.RUnlock方法調(diào)用一定happen before 任意的 rw.Lock(它們happen after 第n個(gè)rw.RLock方法調(diào)用返回之后)
Waitgroup
對(duì)于 Waitgroup b, 對(duì)于其計(jì)數(shù)器不是0的時(shí)候,假如此時(shí)刻之后有一組wg.Add(n),并且我們確信只有最后一組方法調(diào)用使其計(jì)數(shù)器最后復(fù)原為0,那么這組wg.Add 方法調(diào)用一定happen before 這一時(shí)刻之后發(fā)生的wg.Wait
wg.Done()也是wg.Add(-1)
Once
- once.Do方法的執(zhí)行一定happen before 任何一個(gè)once.Do方法的返回
Atomic
沒(méi)有官方的保證
建議是不要依賴atomic保證內(nèi)存的順序
-
5045 歷史悠久的討論,還沒(méi)close
GO并發(fā)編程
Mutex
最常用的sync包下的同步原語(yǔ)之一。
自2008年開(kāi)始,經(jīng)過(guò)了幾次大的修改,加入了公平性和性能綜合的考量、饑餓的處理,今年又進(jìn)行了內(nèi)斂的優(yōu)化,對(duì)功能和性能都有了很好的提升。
內(nèi)部結(jié)構(gòu)使用?state?標(biāo)記是否加鎖、喚醒、饑餓等狀態(tài),使用高位來(lái)記錄等待者的數(shù)量。
雖然?state?是unexported的,但是你可以通過(guò)?unsafe?包hack方式讀取這些狀態(tài)。
Unlock未加鎖或者已解鎖的Mutex會(huì)panic
Mutex不會(huì)比較當(dāng)前請(qǐng)求的goroutine是否已經(jīng)持有這個(gè)鎖,所以可以一個(gè)goorutine Lock,另一個(gè)goroutine Unlock,但是慎用,避免死鎖
非重入鎖, Java程序員轉(zhuǎn)Go容易犯的錯(cuò),會(huì)導(dǎo)致死鎖。 如果想重入,使用擴(kuò)展的同步原語(yǔ)。
RWMutex
讀寫鎖對(duì)于讀進(jìn)行了優(yōu)化,適合寫少讀多的狀態(tài),對(duì)并發(fā)的讀很適合。
如果有g(shù)oroutine持有了RWMutex,那么只可能被一組reader持有,或者被一個(gè)writer持有。
如果已經(jīng)有一組reader持有了讀寫鎖,這個(gè)時(shí)候如果writer調(diào)用Lock,它會(huì)被阻塞。接著如果有reader調(diào)用RLock, 等前面那組readerUnlock后, writer優(yōu)先獲取鎖
不要在遞歸調(diào)用讀鎖。因?yàn)樯弦粭l的規(guī)定,遞歸調(diào)用鎖容易導(dǎo)致死鎖
可以將讀鎖返回成一個(gè)Locker的接口
Cond
類似Monitor,提供了通知的機(jī)制,可以?Broadcast?通知所有?Wait?的goroutine,也可以?Signal?通知某一個(gè)?Wait?的goroutine。
Cond初始化的時(shí)候要傳入一個(gè)Locker接口的實(shí)現(xiàn),這個(gè)Locker可以用來(lái)保護(hù)條件變量。
?Broadcast?和?Signal?不需要加鎖調(diào)用,但是調(diào)用?Wait?的時(shí)候需要加鎖。
?Wait?執(zhí)行中有解鎖重加鎖的過(guò)程,在這個(gè)期間對(duì)臨界區(qū)是沒(méi)有保護(hù)的。
一定要使用?for?循環(huán)來(lái)檢查條件是否滿足,因?yàn)殡S時(shí)都可以觸發(fā)通知。
Waitgroup
也是最常用的sync包下的同步原語(yǔ)之一。
內(nèi)部通過(guò)一個(gè)計(jì)數(shù)器來(lái)記錄waiter。
在?Wait?之前可以設(shè)置這個(gè)計(jì)數(shù)器的數(shù)量。等這個(gè)計(jì)數(shù)器為0的時(shí)候,所有等待的goroutine都都會(huì)解除等待,繼續(xù)執(zhí)行。
Add方法可以增加計(jì)數(shù),也可以傳入負(fù)值減少技術(shù),但是如果計(jì)數(shù)器小于0的情況下會(huì)panic。
Done方法是利用-1實(shí)現(xiàn)的,因此Done的次數(shù)如果多于計(jì)數(shù)器,會(huì)panic。
Wait調(diào)用多次沒(méi)問(wèn)題,只要計(jì)數(shù)器為0,它就不會(huì)阻塞。
并發(fā) Add和Wait會(huì)panic。
前一個(gè)Wait還沒(méi)有完成就Add也會(huì)panic。
所以Waitgroup是可以 重用的,但是一定等前一個(gè)Wait完成后再重用。
Once
用來(lái)初始化一次,比如實(shí)現(xiàn)單例,單元測(cè)試時(shí)環(huán)境的準(zhǔn)備。
不要在傳給Do的函數(shù)中調(diào)用這個(gè)Once,否則會(huì)死鎖。
即使傳入的這個(gè)函數(shù)會(huì)panic,Once也認(rèn)為它已經(jīng)初始化了。
Go單例的實(shí)現(xiàn):
常量
package 變量 (eager)
init函數(shù) (eager)
GetInstance() (lazy)
通過(guò)sync.Once或者類似實(shí)現(xiàn)
A XXX must not be copied after first use.
看上面的同步原語(yǔ)的 godoc,都有這么一句話。對(duì)象使用后就不能被復(fù)制了。
這是因?yàn)槭褂煤筮@些對(duì)象都是有狀態(tài)的,復(fù)制過(guò)去也會(huì)把狀態(tài)復(fù)制過(guò)去,比如已加鎖的狀態(tài),這不是我們期望的。
可以通過(guò)?go vet?工具檢查。
如果你定義的struct也想有這個(gè)功能,可以使用?noCopy?這種經(jīng)濟(jì)的方式,定義Locker接口,讓vet工具也能檢查。
簡(jiǎn)單的復(fù)制是容易看出來(lái)的,很多隱藏的復(fù)制檢查可以通過(guò)工具。
Pool
臨時(shí)對(duì)象池
可能在任何時(shí)候任意的對(duì)象都可能被移除
可以安全地并發(fā)訪問(wèn)
裝箱/拆箱
tcp、數(shù)據(jù)庫(kù)連接池的話不要使用它,使用專門的池。
標(biāo)準(zhǔn)庫(kù)中有的池的實(shí)現(xiàn)使用它,有的需要永久持有的對(duì)象不使用它,而是使用鏈表,比如rpc。
用它做buffer池要注意,避免內(nèi)存泄漏。Pool的官方例子和標(biāo)準(zhǔn)庫(kù)fmt、json中都有這個(gè)坑。標(biāo)準(zhǔn)庫(kù)中已經(jīng)修復(fù)了。
Map
使用空間換時(shí)間的方式,提供下面兩個(gè)場(chǎng)景下的性能:
設(shè)置一次,多次讀,比如cache
多個(gè)goroutine并發(fā)的讀、寫、更新不同的key
有以下的考量:
裝箱/拆箱
Range進(jìn)行遍歷,可能會(huì)加鎖
沒(méi)有Len方法,并且也不會(huì)添加
ReentrantLock
標(biāo)準(zhǔn)庫(kù)sync下的Mutex是不能重入的,如果想實(shí)現(xiàn)重入的話,可以利用:
goid:用來(lái)標(biāo)記誰(shuí)持有了當(dāng)前鎖,重入了幾次
全局id:或者你自己維護(hù)一個(gè)全局id,但是實(shí)現(xiàn)的結(jié)構(gòu)不再滿足Locker接口
可重入鎖也叫做遞歸鎖,但是叫可重入鎖更準(zhǔn)確些,因?yàn)榭芍厝肟刹恢贿f歸這么一種情況。
Semaphore
Dijkstra提出并發(fā)訪問(wèn)通用資源的并發(fā)原語(yǔ),使用PV原語(yǔ)提供對(duì)臨界區(qū)的保護(hù)。
二進(jìn)制(取值0,1)的semaphore提供了鎖的功能。
計(jì)數(shù)器semaphore提供了對(duì)一組資源的保護(hù)。
包 ?golang.org/x/sync/semaphore?。
標(biāo)準(zhǔn)庫(kù)內(nèi)部的semaphore提供了休眠/喚醒的功能,用來(lái)實(shí)現(xiàn)基本同步原語(yǔ)的阻塞。
SingleFlight
并發(fā)的訪問(wèn)同一組資源的時(shí)候,只允許一個(gè)請(qǐng)求進(jìn)行,這個(gè)請(qǐng)求把結(jié)果告訴其它等待者,避免雪崩的現(xiàn)象。
比如cache 失效的時(shí)候,只允許一個(gè)goroutine從數(shù)據(jù)庫(kù)中撈數(shù)據(jù)回種,避免雪崩對(duì)數(shù)據(jù)庫(kù)的影響。
擴(kuò)展庫(kù)中提供。
ErrGroup
應(yīng)用于 half sync/half async的場(chǎng)景(這個(gè)設(shè)計(jì)模式以后有機(jī)會(huì)再介紹)。
有一組異步的任務(wù)需要處理,利用這個(gè)原語(yǔ)可以等待所有的異步任務(wù)完成,并獲取第一個(gè)錯(cuò)誤。
如果想得到所有的錯(cuò)誤,利用額外的map變量進(jìn)行記錄。
使用Context可以實(shí)現(xiàn)遇到第一個(gè)錯(cuò)誤就返回。
擴(kuò)展包中提供。
bilibili擴(kuò)展了這個(gè)原語(yǔ),提供了限定并發(fā)數(shù)量的功能。
SpinLock
自旋鎖
有時(shí)候很高效,因?yàn)楫?dāng)前CPU中運(yùn)行的goroutine更有機(jī)會(huì)獲取到鎖
不公平
需要處理器忙等待
應(yīng)用于競(jìng)爭(zhēng)不是很激烈的狀態(tài)
fslock
文件鎖, 可以控制多個(gè)進(jìn)程之間的同步。
concurrent-map
類似Java中的ConcurrentMap的設(shè)計(jì)思想,將key劃分成一定數(shù)量的shard,每個(gè)shard一個(gè)鎖,減少鎖的競(jìng)爭(zhēng)。
相對(duì)于sync.Map,可以應(yīng)用寫/刪除/添加更頻繁的場(chǎng)景。
原子操作
保證操作是原子的。
操作的數(shù)據(jù)
int32
int64
uint32
uint64
uintptr
unsafe.Pointer
操作方法
AddXXX (整數(shù)類型)
CompareAndSwapXXX:cas
LoadXXX:讀取
StoreXXX:存儲(chǔ)
SwapXXX:交換
Subtract
有符號(hào)的類型,可以使用Add負(fù)數(shù)
無(wú)符號(hào)的類型,可以使用AddUint32(&x, ^uint32(c-1)),AddUint64(&x, ^uint64(c-1))
無(wú)符號(hào)類型減一, AddUint32(&x, ^uint32(0)), AddUint64(&x, ^uint64(0))
Value
一個(gè)通用的對(duì)象,可以很方便的對(duì)struct等類型進(jìn)行原子存儲(chǔ)和加載。
由于不同的架構(gòu)下對(duì)原子操作的支持是不一樣的,有些架構(gòu)師是不支持的。
fxxk...shiit..放棄
參考
進(jìn)程、線程、協(xié)程和goroutine
https://studygolang.com/articles/20327
golang線程和協(xié)程
https://juejin.im/post/5d9a9c12e51d45781420fb7e
go調(diào)度器系列
https://segmentfault.com/a/1190000018451422
https://segmentfault.com/a/1190000018672300
https://segmentfault.com/a/1190000018775901
https://segmentfault.com/a/1190000018876007
golang-調(diào)度剖析
https://segmentfault.com/a/1190000016038785
https://segmentfault.com/a/1190000016611742
https://segmentfault.com/a/1190000017333717
深入理解go調(diào)度
https://www.cnblogs.com/sunsky303/p/9705727.html
并發(fā)模型比較
https://gobomb.github.io/post/high-concurrency-model/
go編發(fā)編程
https://colobu.com/2019/04/28/gopher-2019-concurrent-in-action/#%E5%86%85%E5%AE%B9%E5%88%92%E5%88%86