久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

goroutine背后的系統(tǒng)知識(shí)

 鳳凰苑兇真 2016-08-02

Go語(yǔ)言從誕生到普及已經(jīng)三年了,先行者大都是Web開(kāi)發(fā)的背景,,也有了一些普及型的書籍,,可系統(tǒng)開(kāi)發(fā)背景的人在學(xué)習(xí)這些書籍的時(shí)候,總有語(yǔ)焉不詳?shù)母杏X(jué),,網(wǎng)上也有若干流傳甚廣的文章,,可其中或多或少總有些與事實(shí)不符的技術(shù)描述。希望這篇文章能為比較缺少系統(tǒng)編程背景的Web開(kāi)發(fā)人員介紹一下goroutine背后的系統(tǒng)知識(shí),。

1. 操作系統(tǒng)與運(yùn)行庫(kù)

對(duì)于普通的電腦用戶來(lái)說(shuō),,能理解應(yīng)用程序是運(yùn)行在操作系統(tǒng)之上就足夠了,可對(duì)于開(kāi)發(fā)者,,我們還需要了解我們寫的程序是如何在操作系統(tǒng)之上運(yùn)行起來(lái)的,,操作系統(tǒng)如何為應(yīng)用程序提供服務(wù),這樣我們才能分清楚哪些服務(wù)是操作系統(tǒng)提供的,,而哪些服務(wù)是由我們所使用的語(yǔ)言的運(yùn)行庫(kù)提供的,。

除了內(nèi)存管理、文件管理,、進(jìn)程管理,、外設(shè)管理等等內(nèi)部模塊以外,操作系統(tǒng)還提供了許多外部接口供應(yīng)用程序使用,,這些接口就是所謂的“系統(tǒng)調(diào)用”,。從DOS時(shí)代開(kāi)始,系統(tǒng)調(diào)用就是通過(guò)軟中斷的形式來(lái)提供,,也就是著名的INT 21,,程序把需要調(diào)用的功能編號(hào)放入AH寄存器,把參數(shù)放入其他指定的寄存器,,然后調(diào)用INT 21,,中斷返回后,程序從指定的寄存器(通常是AL)里取得返回值。這樣的做法一直到奔騰2也就是P6出來(lái)之前都沒(méi)有變,,譬如windows通過(guò)INT 2E提供系統(tǒng)調(diào)用,,Linux則是INT 80,只不過(guò)后來(lái)的寄存器比以前大一些,,而且可能再多一層跳轉(zhuǎn)表查詢,。后來(lái),Intel和AMD分別提供了效率更高的SYSENTER/SYSEXIT和SYSCALL/SYSRET指令來(lái)代替之前的中斷方式,,略過(guò)了耗時(shí)的特權(quán)級(jí)別檢查以及寄存器壓棧出棧的操作,,直接完成從RING 3代碼段到RING 0代碼段的轉(zhuǎn)換。

系統(tǒng)調(diào)用都提供什么功能呢,?用操作系統(tǒng)的名字加上對(duì)應(yīng)的中斷編號(hào)到谷歌上一查就可以得到完整的列表 (Windows, Linux),,這個(gè)列表就是操作系統(tǒng)和應(yīng)用程序之間溝通的協(xié)議,如果需要超出此協(xié)議的功能,,我們就只能在自己的代碼里去實(shí)現(xiàn),,譬如,對(duì)于內(nèi)存管理,,操作系統(tǒng)只提供進(jìn)程級(jí)別的內(nèi)存段的管理,,譬如Windows的virtualmemory系列,或是Linux的brk,,操作系統(tǒng)不會(huì)去在乎應(yīng)用程序如何為新建對(duì)象分配內(nèi)存,,或是如何做垃圾回收,這些都需要應(yīng)用程序自己去實(shí)現(xiàn),。如果超出此協(xié)議的功能無(wú)法自己實(shí)現(xiàn),,那我們就說(shuō)該操作系統(tǒng)不支持該功能,舉個(gè)例子,,Linux在2.6之前是不支持多線程的,,無(wú)論如何在程序里模擬,我們都無(wú)法做出多個(gè)可以同時(shí)運(yùn)行的并符合POSIX 1003.1c語(yǔ)義標(biāo)準(zhǔn)的調(diào)度單元,。

可是,,我們寫程序并不需要去調(diào)用中斷或是SYSCALL指令,這是因?yàn)椴僮飨到y(tǒng)提供了一層封裝,,在Windows上,,它是NTDLL.DLL,也就是常說(shuō)的Native API,,我們不但不需要去直接調(diào)用INT 2E或SYSCALL,,準(zhǔn)確的說(shuō),我們不能直接去調(diào)用INT 2E或SYSCALL,,因?yàn)閃indows并沒(méi)有公開(kāi)其調(diào)用規(guī)范,,直接使用INT 2E或SYSCALL無(wú)法保證未來(lái)的兼容性。在Linux上則沒(méi)有這個(gè)問(wèn)題,系統(tǒng)調(diào)用的列表都是公開(kāi)的,,而且Linus非常看重兼容性,,不會(huì)去做任何更改,,glibc里甚至專門提供了syscall(2)來(lái)方便用戶直接用編號(hào)調(diào)用,不過(guò),,為了解決glibc和內(nèi)核之間不同版本兼容性帶來(lái)的麻煩,,以及為了提高某些調(diào)用的效率(譬如_NR gettimeofday),Linux上還是對(duì)部分系統(tǒng)調(diào)用做了一層封裝,,就是VDSO (早期叫linux-gate.so),。

可是,我們寫程序也很少直接調(diào)用NTDLL或者VDSO,,而是通過(guò)更上一層的封裝,,這一層處理了參數(shù)準(zhǔn)備和返回值格式轉(zhuǎn)換、以及出錯(cuò)處理和錯(cuò)誤代碼轉(zhuǎn)換,,這就是我們所使用語(yǔ)言的運(yùn)行庫(kù),,對(duì)于C語(yǔ)言,Linux上是glibc,,Windows上是kernel32(或調(diào)用msvcrt),,對(duì)于其他語(yǔ)言,譬如Java,,則是JRE,,這些“其他語(yǔ)言”的運(yùn)行庫(kù)通常最終還是調(diào)用glibc或kernel32。

“運(yùn)行庫(kù)”這個(gè)詞其實(shí)不止包括用于和編譯后的目標(biāo)執(zhí)行程序進(jìn)行鏈接的庫(kù)文件,,也包括了腳本語(yǔ)言或字節(jié)碼解釋型語(yǔ)言的運(yùn)行環(huán)境,,譬如Python,C#的CLR,,Java的JRE,。

對(duì)系統(tǒng)調(diào)用的封裝只是運(yùn)行庫(kù)的很小一部分功能,運(yùn)行庫(kù)通常還提供了諸如字符串處理,、數(shù)學(xué)計(jì)算,、常用數(shù)據(jù)結(jié)構(gòu)容器等等不需要操作系統(tǒng)支持的功能,同時(shí),,運(yùn)行庫(kù)也會(huì)對(duì)操作系統(tǒng)支持的功能提供更易用更高級(jí)的封裝,,譬如帶緩存和格式的IO、線程池,。

所以,,在我們說(shuō)“某某語(yǔ)言新增了某某功能”的時(shí)候,通常是這么幾種可能:

  1. 支持新的語(yǔ)義或語(yǔ)法,從而便于我們描述和解決問(wèn)題,。譬如Java的泛型,、Annotation、lambda表達(dá)式,。
  2. 提供了新的工具或類庫(kù),,減少了我們開(kāi)發(fā)的代碼量。譬如Python 2.7的argparse
  3. 對(duì)系統(tǒng)調(diào)用有了更良好更全面的封裝,,使我們可以做到以前在這個(gè)語(yǔ)言環(huán)境里做不到或很難做到的事情,。譬如Java NIO

但任何一門語(yǔ)言,包括其運(yùn)行庫(kù)和運(yùn)行環(huán)境,,都不可能創(chuàng)造出操作系統(tǒng)不支持的功能,,Go語(yǔ)言也是這樣,不管它的特性描述看起來(lái)多么炫麗,,那必然都是其他語(yǔ)言也可以做到的,,只不過(guò)Go提供了更方便更清晰的語(yǔ)義和支持,提高了開(kāi)發(fā)的效率,。

2. 并發(fā)與并行 (Concurrency and Parallelism)

并發(fā)是指程序的邏輯結(jié)構(gòu),。非并發(fā)的程序就是一根竹竿捅到底,只有一個(gè)邏輯控制流,,也就是順序執(zhí)行的(Sequential)程序,,在任何時(shí)刻,程序只會(huì)處在這個(gè)邏輯控制流的某個(gè)位置,。而如果某個(gè)程序有多個(gè)獨(dú)立的邏輯控制流,,也就是可以同時(shí)處理(deal)多件事情,我們就說(shuō)這個(gè)程序是并發(fā)的,。這里的“同時(shí)”,,并不一定要是真正在時(shí)鐘的某一時(shí)刻(那是運(yùn)行狀態(tài)而不是邏輯結(jié)構(gòu)),而是指:如果把這些邏輯控制流畫成時(shí)序流程圖,,它們?cè)跁r(shí)間線上是可以重疊的,。

并行是指程序的運(yùn)行狀態(tài)。如果一個(gè)程序在某一時(shí)刻被多個(gè)CPU流水線同時(shí)進(jìn)行處理,,那么我們就說(shuō)這個(gè)程序是以并行的形式在運(yùn)行,。(嚴(yán)格意義上講,我們不能說(shuō)某程序是“并行”的,,因?yàn)椤安⑿小辈皇敲枋龀绦虮旧?,而是描述程序的運(yùn)行狀態(tài),但這篇小文里就不那么咬文嚼字,,以下說(shuō)到“并行”的時(shí)候,,就是指代“以并行的形式運(yùn)行”)顯然,,并行一定是需要硬件支持的。

而且不難理解:

  1. 并發(fā)是并行的必要條件,,如果一個(gè)程序本身就不是并發(fā)的,,也就是只有一個(gè)邏輯控制流,那么我們不可能讓其被并行處理,。

  2. 并發(fā)不是并行的充分條件,,一個(gè)并發(fā)的程序,如果只被一個(gè)CPU流水線進(jìn)行處理(通過(guò)分時(shí)),,那么它就不是并行的。

  3. 并發(fā)只是更符合現(xiàn)實(shí)問(wèn)題本質(zhì)的表達(dá)方式,,并發(fā)的最初目的是簡(jiǎn)化代碼邏輯,,而不是使程序運(yùn)行的更快;

這幾段略微抽象,,我們可以用一個(gè)最簡(jiǎn)單的例子來(lái)把這些概念實(shí)例化:用C語(yǔ)言寫一個(gè)最簡(jiǎn)單的HelloWorld,,它就是非并發(fā)的,如果我們建立多個(gè)線程,,每個(gè)線程里打印一個(gè)HelloWorld,,那么這個(gè)程序就是并發(fā)的,如果這個(gè)程序運(yùn)行在老式的單核CPU上,,那么這個(gè)并發(fā)程序還不是并行的,,如果我們用多核多CPU且支持多任務(wù)的操作系統(tǒng)來(lái)運(yùn)行它,那么這個(gè)并發(fā)程序就是并行的,。

還有一個(gè)略微復(fù)雜的例子,,更能說(shuō)明并發(fā)不一定可以并行,而且并發(fā)不是為了效率,,就是Go語(yǔ)言例子里計(jì)算素?cái)?shù)的sieve.go,。我們從小到大針對(duì)每一個(gè)因子啟動(dòng)一個(gè)代碼片段,如果當(dāng)前驗(yàn)證的數(shù)能被當(dāng)前因子除盡,,則該數(shù)不是素?cái)?shù),,如果不能,則把該數(shù)發(fā)送給下一個(gè)因子的代碼片段,,直到最后一個(gè)因子也無(wú)法除盡,,則該數(shù)為素?cái)?shù),我們?cè)賳?dòng)一個(gè)它的代碼片段,,用于驗(yàn)證更大的數(shù)字,。這是符合我們計(jì)算素?cái)?shù)的邏輯的,而且每個(gè)因子的代碼處理片段都是相同的,,所以程序非常的簡(jiǎn)潔,,但它無(wú)法被并行,,因?yàn)槊總€(gè)片段都依賴于前一個(gè)片段的處理結(jié)果和輸出。

并發(fā)可以通過(guò)以下方式做到:

  1. 顯式地定義并觸發(fā)多個(gè)代碼片段,,也就是邏輯控制流,,由應(yīng)用程序或操作系統(tǒng)對(duì)它們進(jìn)行調(diào)度。它們可以是獨(dú)立無(wú)關(guān)的,,也可以是相互依賴需要交互的,,譬如上面提到的素?cái)?shù)計(jì)算,其實(shí)它也是個(gè)經(jīng)典的生產(chǎn)者和消費(fèi)者的問(wèn)題:兩個(gè)邏輯控制流A和B,,A產(chǎn)生輸出,,當(dāng)有了輸出后,B取得A的輸出進(jìn)行處理,。線程只是實(shí)現(xiàn)并發(fā)的其中一個(gè)手段,,除此之外,運(yùn)行庫(kù)或是應(yīng)用程序本身也有多種手段來(lái)實(shí)現(xiàn)并發(fā),,這是下節(jié)的主要內(nèi)容,。

  2. 隱式地放置多個(gè)代碼片段,在系統(tǒng)事件發(fā)生時(shí)觸發(fā)執(zhí)行相應(yīng)的代碼片段,,也就是事件驅(qū)動(dòng)的方式,,譬如某個(gè)端口或管道接收到了數(shù)據(jù)(多路IO的情況下),再譬如進(jìn)程接收到了某個(gè)信號(hào)(signal),。

并行可以在四個(gè)層面上做到:

  1. 多臺(tái)機(jī)器,。自然我們就有了多個(gè)CPU流水線,譬如Hadoop集群里的MapReduce任務(wù),。

  2. 多CPU,。不管是真的多顆CPU還是多核還是超線程,總之我們有了多個(gè)CPU流水線,。

  3. 單CPU核里的ILP(Instruction-level parallelism),,指令級(jí)并行。通過(guò)復(fù)雜的制造工藝和對(duì)指令的解析以及分支預(yù)測(cè)和亂序執(zhí)行,,現(xiàn)在的CPU可以在單個(gè)時(shí)鐘周期內(nèi)執(zhí)行多條指令,,從而,即使是非并發(fā)的程序,,也可能是以并行的形式執(zhí)行,。

  4. 單指令多數(shù)據(jù)(Single instruction, multiple data. SIMD),為了多媒體數(shù)據(jù)的處理,,現(xiàn)在的CPU的指令集支持單條指令對(duì)多條數(shù)據(jù)進(jìn)行操作,。

其中,1牽涉到分布式處理,,包括數(shù)據(jù)的分布和任務(wù)的同步等等,,而且是基于網(wǎng)絡(luò)的,。3和4通常是編譯器和CPU的開(kāi)發(fā)人員需要考慮的。這里我們說(shuō)的并行主要針對(duì)第2種:?jiǎn)闻_(tái)機(jī)器內(nèi)的多核CPU并行,。

關(guān)于并發(fā)與并行的問(wèn)題,,Go語(yǔ)言的作者Rob Pike專門就此寫過(guò)一個(gè)幻燈片:http://talks./2012/waza.slide

在CMU那本著名的《Computer Systems: A Programmer’s Perspective》里的這張圖也非常直觀清晰:

3. 線程的調(diào)度

上一節(jié)主要說(shuō)的是并發(fā)和并行的概念,而線程是最直觀的并發(fā)的實(shí)現(xiàn),,這一節(jié)我們主要說(shuō)操作系統(tǒng)如何讓多個(gè)線程并發(fā)的執(zhí)行,,當(dāng)然在多CPU的時(shí)候,也就是并行的執(zhí)行,。我們不討論進(jìn)程,,進(jìn)程的意義是“隔離的執(zhí)行環(huán)境”,而不是“單獨(dú)的執(zhí)行序列”,。

我們首先需要理解IA-32 CPU的指令控制方式,,這樣才能理解如何在多個(gè)指令序列(也就是邏輯控制流)之間進(jìn)行切換。CPU通過(guò)CS:EIP寄存器的值確定下一條指令的位置,,但是CPU并不允許直接使用MOV指令來(lái)更改EIP的值,必須通過(guò)JMP系列指令,、CALL/RET指令,、或INT中斷指令來(lái)實(shí)現(xiàn)代碼的跳轉(zhuǎn);在指令序列間切換的時(shí)候,,除了更改EIP之外,,我們還要保證代碼可能會(huì)使用到的各個(gè)寄存器的值,尤其是棧指針SS:ESP,,以及EFLAGS標(biāo)志位等,,都能夠恢復(fù)到目標(biāo)指令序列上次執(zhí)行到這個(gè)位置時(shí)候的狀態(tài)。

線程是操作系統(tǒng)對(duì)外提供的服務(wù),,應(yīng)用程序可以通過(guò)系統(tǒng)調(diào)用讓操作系統(tǒng)啟動(dòng)線程,,并負(fù)責(zé)隨后的線程調(diào)度和切換。我們先考慮單顆單核CPU,,操作系統(tǒng)內(nèi)核與應(yīng)用程序其實(shí)是也是在共享同一個(gè)CPU,,當(dāng)EIP在應(yīng)用程序代碼段的時(shí)候,內(nèi)核并沒(méi)有控制權(quán),,內(nèi)核并不是一個(gè)進(jìn)程或線程,,內(nèi)核只是以保護(hù)模式運(yùn)行的,代碼段權(quán)限為RING 0的內(nèi)存中的程序,,只有當(dāng)產(chǎn)生中斷或是應(yīng)用程序呼叫系統(tǒng)調(diào)用的時(shí)候,,控制權(quán)才轉(zhuǎn)移到內(nèi)核,在內(nèi)核里,,所有代碼都在同一個(gè)地址空間,,為了給不同的線程提供服務(wù),,內(nèi)核會(huì)為每一個(gè)線程建立一個(gè)內(nèi)核堆棧,這是線程切換的關(guān)鍵,。通常,,內(nèi)核會(huì)在時(shí)鐘中斷里或系統(tǒng)調(diào)用返回前(考慮到性能,通常是在不頻繁發(fā)生的系統(tǒng)調(diào)用返回前),,對(duì)整個(gè)系統(tǒng)的線程進(jìn)行調(diào)度,,計(jì)算當(dāng)前線程的剩余時(shí)間片,如果需要切換,,就在“可運(yùn)行”的線程隊(duì)列里計(jì)算優(yōu)先級(jí),,選出目標(biāo)線程后,則保存當(dāng)前線程的運(yùn)行環(huán)境,,并恢復(fù)目標(biāo)線程的運(yùn)行環(huán)境,,其中最重要的,就是切換堆棧指針ESP,,然后再把EIP指向目標(biāo)線程上次被移出CPU時(shí)的指令,。Linux內(nèi)核在實(shí)現(xiàn)線程切換時(shí),耍了個(gè)花槍,,它并不是直接JMP,,而是先把ESP切換為目標(biāo)線程的內(nèi)核棧,把目標(biāo)線程的代碼地址壓棧,,然后JMP到__switch_to(),,相當(dāng)于偽造了一個(gè)CALL switch_to()指令,然后,,在switch_to()的最后使用RET指令返回,,這樣就把棧里的目標(biāo)線程的代碼地址放入了EIP,接下來(lái)CPU就開(kāi)始執(zhí)行目標(biāo)線程的代碼了,,其實(shí)也就是上次停在switch_to這個(gè)宏展開(kāi)的地方,。

這里需要補(bǔ)充幾點(diǎn):(1) 雖然IA-32提供了TSS (Task State Segment),試圖簡(jiǎn)化操作系統(tǒng)進(jìn)行線程調(diào)度的流程,,但由于其效率低下,,而且并不是通用標(biāo)準(zhǔn),不利于移植,,所以主流操作系統(tǒng)都沒(méi)有去利用TSS,。更嚴(yán)格的說(shuō),其實(shí)還是用了TSS,,因?yàn)橹挥型ㄟ^(guò)TSS才能把堆棧切換到內(nèi)核堆棧指針SS0:ESP0,,但除此之外的TSS的功能就完全沒(méi)有被使用了。(2) 線程從用戶態(tài)進(jìn)入內(nèi)核的時(shí)候,,相關(guān)的寄存器以及用戶態(tài)代碼段的EIP已經(jīng)保存了一次,,所以,,在上面所說(shuō)的內(nèi)核態(tài)線程切換時(shí),需要保存和恢復(fù)的內(nèi)容并不多,。(3) 以上描述的都是搶占式(preemptively)的調(diào)度方式,,內(nèi)核以及其中的硬件驅(qū)動(dòng)也會(huì)在等待外部資源可用的時(shí)候主動(dòng)調(diào)用schedule(),,用戶態(tài)的代碼也可以通過(guò)sched_yield()系統(tǒng)調(diào)用主動(dòng)發(fā)起調(diào)度,,讓出CPU。

現(xiàn)在我們一臺(tái)普通的PC或服務(wù)里通常都有多顆CPU (physical package),,每顆CPU又有多個(gè)核 (processor core),,每個(gè)核又可以支持超線程 (two logical processors for each core),,也就是邏輯處理器。每個(gè)邏輯處理器都有自己的一套完整的寄存器,,其中包括了CS:EIP和SS:ESP,,從而,以操作系統(tǒng)和應(yīng)用的角度來(lái)看,,每個(gè)邏輯處理器都是一個(gè)單獨(dú)的流水線,。在多處理器的情況下,線程切換的原理和流程其實(shí)和單處理器時(shí)是基本一致的,,內(nèi)核代碼只有一份,,當(dāng)某個(gè)CPU上發(fā)生時(shí)鐘中斷或是系統(tǒng)調(diào)用時(shí),該CPU的CS:EIP和控制權(quán)又回到了內(nèi)核,,內(nèi)核根據(jù)調(diào)度策略的結(jié)果進(jìn)行線程切換。但在這個(gè)時(shí)候,,如果我們的程序用線程實(shí)現(xiàn)了并發(fā),,那么操作系統(tǒng)可以使我們的程序在多個(gè)CPU上實(shí)現(xiàn)并行。

這里也需要補(bǔ)充兩點(diǎn):(1) 多核的場(chǎng)景里,,各個(gè)核之間并不是完全對(duì)等的,,譬如在同一個(gè)核上的兩個(gè)超線程是共享L1/L2緩存的;在有NUMA支持的場(chǎng)景里,,每個(gè)核訪問(wèn)內(nèi)存不同區(qū)域的延遲是不一樣的,;所以,多核場(chǎng)景里的線程調(diào)度又引入了“調(diào)度域”(scheduling domains)的概念,,但這不影響我們理解線程切換機(jī)制,。(2) 多核的場(chǎng)景下,中斷發(fā)給哪個(gè)CPU,?軟中斷(包括除以0,,缺頁(yè)異常,INT指令)自然是在觸發(fā)該中斷的CPU上產(chǎn)生,,而硬中斷則又分兩種情況,,一種是每個(gè)CPU自己產(chǎn)生的中斷,,譬如時(shí)鐘,這是每個(gè)CPU處理自己的,,還有一種是外部中斷,,譬如IO,可以通過(guò)APIC來(lái)指定其送給哪個(gè)CPU,;因?yàn)檎{(diào)度程序只能控制當(dāng)前的CPU,,所以,如果IO中斷沒(méi)有進(jìn)行均勻的分配的話,,那么和IO相關(guān)的線程就只能在某些CPU上運(yùn)行,,導(dǎo)致CPU負(fù)載不均,進(jìn)而影響整個(gè)系統(tǒng)的效率,。

4. 并發(fā)編程框架

以上大概介紹了一個(gè)用多線程來(lái)實(shí)現(xiàn)并發(fā)的程序是如何被操作系統(tǒng)調(diào)度以及并行執(zhí)行(在有多個(gè)邏輯處理器時(shí)),,同時(shí)大家也可以看到,代碼片段或者說(shuō)邏輯控制流的調(diào)度和切換其實(shí)并不神秘,,理論上,,我們也可以不依賴操作系統(tǒng)和其提供的線程,在自己程序的代碼段里定義多個(gè)片段,,然后在我們自己程序里對(duì)其進(jìn)行調(diào)度和切換,。

為了描述方便,我們接下來(lái)把“代碼片段”稱為“任務(wù)”,。

和內(nèi)核的實(shí)現(xiàn)類似,,只是我們不需要考慮中斷和系統(tǒng)調(diào)用,那么,,我們的程序本質(zhì)上就是一個(gè)循環(huán),,這個(gè)循環(huán)本身就是調(diào)度程序schedule(),我們需要維護(hù)一個(gè)任務(wù)的列表,,根據(jù)我們定義的策略,,先進(jìn)先出或是有優(yōu)先級(jí)等等,每次從列表里挑選出一個(gè)任務(wù),,然后恢復(fù)各個(gè)寄存器的值,,并且JMP到該任務(wù)上次被暫停的地方,所有這些需要保存的信息都可以作為該任務(wù)的屬性,,存放在任務(wù)列表里,。

看起來(lái)很簡(jiǎn)單啊,可是我們還需要解決幾個(gè)問(wèn)題:

(1) 我們運(yùn)行在用戶態(tài),,是沒(méi)有中斷或系統(tǒng)調(diào)用這樣的機(jī)制來(lái)打斷代碼執(zhí)行的,,那么,一旦我們的schedule()代碼把控制權(quán)交給了任務(wù)的代碼,我們下次的調(diào)度在什么時(shí)候發(fā)生,?答案是,,不會(huì)發(fā)生,只有靠任務(wù)主動(dòng)調(diào)用schedule(),,我們才有機(jī)會(huì)進(jìn)行調(diào)度,,所以,這里的任務(wù)不能像線程一樣依賴內(nèi)核調(diào)度從而毫無(wú)顧忌的執(zhí)行,,我們的任務(wù)里一定要顯式的調(diào)用schedule(),,這就是所謂的協(xié)作式(cooperative)調(diào)度。(雖然我們可以通過(guò)注冊(cè)信號(hào)處理函數(shù)來(lái)模擬內(nèi)核里的時(shí)鐘中斷并取得控制權(quán),,可問(wèn)題在于,,信號(hào)處理函數(shù)是由內(nèi)核調(diào)用的,在其結(jié)束的時(shí)候,,內(nèi)核重新獲得控制權(quán),,隨后返回用戶態(tài)并繼續(xù)沿著信號(hào)發(fā)生時(shí)被中斷的代碼路徑執(zhí)行,從而我們無(wú)法在信號(hào)處理函數(shù)內(nèi)進(jìn)行任務(wù)切換)

(2) 堆棧,。和內(nèi)核調(diào)度線程的原理一樣,,我們也需要為每個(gè)任務(wù)單獨(dú)分配堆棧,并且把其堆棧信息保存在任務(wù)屬性里,,在任務(wù)切換時(shí)也保存或恢復(fù)當(dāng)前的SS:ESP,。任務(wù)堆棧的空間可以是在當(dāng)前線程的堆棧上分配,也可以是在堆上分配,,但通常是在堆上分配比較好:幾乎沒(méi)有大小或任務(wù)總數(shù)的限制,、堆棧大小可以動(dòng)態(tài)擴(kuò)展(gcc有split stack,但太復(fù)雜了),、便于把任務(wù)切換到其他線程,。

到這里,我們大概知道了如何構(gòu)造一個(gè)并發(fā)的編程框架,,可如何讓任務(wù)可以并行的在多個(gè)邏輯處理器上執(zhí)行呢?只有內(nèi)核才有調(diào)度CPU的權(quán)限,,所以,,我們還是必須通過(guò)系統(tǒng)調(diào)用創(chuàng)建線程,才可以實(shí)現(xiàn)并行,。在多線程處理多任務(wù)的時(shí)候,,我們還需要考慮幾個(gè)問(wèn)題:

(1) 如果某個(gè)任務(wù)發(fā)起了一個(gè)系統(tǒng)調(diào)用,譬如長(zhǎng)時(shí)間等待IO,,那當(dāng)前線程就被內(nèi)核放入了等待調(diào)度的隊(duì)列,,豈不是讓其他任務(wù)都沒(méi)有機(jī)會(huì)執(zhí)行?

在單線程的情況下,我們只有一個(gè)解決辦法,,就是使用非阻塞的IO系統(tǒng)調(diào)用,,并讓出CPU,然后在schedule()里統(tǒng)一進(jìn)行輪詢,,有數(shù)據(jù)時(shí)切換回該fd對(duì)應(yīng)的任務(wù),;效率略低的做法是不進(jìn)行統(tǒng)一輪詢,讓各個(gè)任務(wù)在輪到自己執(zhí)行時(shí)再次用非阻塞方式進(jìn)行IO,,直到有數(shù)據(jù)可用,。

如果我們采用多線程來(lái)構(gòu)造我們整個(gè)的程序,那么我們可以封裝系統(tǒng)調(diào)用的接口,,當(dāng)某個(gè)任務(wù)進(jìn)入系統(tǒng)調(diào)用時(shí),,我們就把當(dāng)前線程留給它(暫時(shí))獨(dú)享,并開(kāi)啟新的線程來(lái)處理其他任務(wù),。

(2) 任務(wù)同步,。譬如我們上節(jié)提到的生產(chǎn)者和消費(fèi)者的例子,如何讓消費(fèi)者在數(shù)據(jù)還沒(méi)有被生產(chǎn)出來(lái)的時(shí)候進(jìn)入等待,,并且在數(shù)據(jù)可用時(shí)觸發(fā)消費(fèi)者繼續(xù)執(zhí)行呢,?

在單線程的情況下,我們可以定義一個(gè)結(jié)構(gòu),,其中有變量用于存放交互數(shù)據(jù)本身,,以及數(shù)據(jù)的當(dāng)前可用狀態(tài),以及負(fù)責(zé)讀寫此數(shù)據(jù)的兩個(gè)任務(wù)的編號(hào),。然后我們的并發(fā)編程框架再提供read和write方法供任務(wù)調(diào)用,,在read方法里,我們循環(huán)檢查數(shù)據(jù)是否可用,,如果數(shù)據(jù)還不可用,,我們就調(diào)用schedule()讓出CPU進(jìn)入等待;在write方法里,,我們往結(jié)構(gòu)里寫入數(shù)據(jù),,更改數(shù)據(jù)可用狀態(tài),然后返回,;在schedule()里,,我們檢查數(shù)據(jù)可用狀態(tài),如果可用,,則激活需要讀取此數(shù)據(jù)的任務(wù),,該任務(wù)繼續(xù)循環(huán)檢測(cè)數(shù)據(jù)是否可用,發(fā)現(xiàn)可用,,讀取,,更改狀態(tài)為不可用,返回。代碼的簡(jiǎn)單邏輯如下:

struct chan {
    bool ready,
    int data
};

int read (struct chan *c) {
    while (1) {
        if (c->ready) {
            c->ready = false;
            return c->data;
        } else {
            schedule();
        }
    }
}

void write (struct chan *c, int i) {
    while (1) {
        if (c->ready) {
            schedule(); 
        } else {
            c->data = i;
            c->ready = true;
            schedule(); // optional
            return;
        }
    }
}

很顯然,,如果是多線程的話,,我們需要通過(guò)線程庫(kù)或系統(tǒng)調(diào)用提供的同步機(jī)制來(lái)保護(hù)對(duì)這個(gè)結(jié)構(gòu)體內(nèi)數(shù)據(jù)的訪問(wèn)。

以上就是最簡(jiǎn)化的一個(gè)并發(fā)框架的設(shè)計(jì)考慮,,在我們實(shí)際開(kāi)發(fā)工作中遇到的并發(fā)框架可能由于語(yǔ)言和運(yùn)行庫(kù)的不同而有所不同,,在功能和易用性上也可能各有取舍,但底層的原理都是殊途同歸,。

譬如,,glic里的getcontext/setcontext/swapcontext系列庫(kù)函數(shù)可以方便的用來(lái)保存和恢復(fù)任務(wù)執(zhí)行狀態(tài);Windows提供了Fiber系列的SDK API,;這二者都不是系統(tǒng)調(diào)用,,getcontext和setcontext的man page雖然是在section 2,但那只是SVR4時(shí)的歷史遺留問(wèn)題,,其實(shí)現(xiàn)代碼是在glibc而不是kernel,;CreateFiber.aspx)是在kernel32里提供的,NTDLL

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購(gòu)買等信息,,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請(qǐng)點(diǎn)擊一鍵舉報(bào),。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多