摘要: 本文介紹協(xié)程、線(xiàn)程及它們執(zhí)行的上下文等概念,同時(shí)給出注意事項(xiàng)。協(xié)程是用戶(hù)級(jí)的任務(wù)調(diào)度,線(xiàn)程是內(nèi)核級(jí)的任務(wù)調(diào)度,而任務(wù)調(diào)度過(guò)程都涉及到上下文切換(保存與恢復(fù)),本文將從較為深刻的角度來(lái)闡述這些概念,及其相互關(guān)系。
線(xiàn)程在現(xiàn)代的系統(tǒng)里扮演的角色很重要,幾乎一個(gè)現(xiàn)代一點(diǎn)的,稍微復(fù)雜大型一點(diǎn)的程序,往往都會(huì)引入線(xiàn)程,實(shí)現(xiàn)某種意義的并行。線(xiàn)程存在廣泛的支持,從操作系統(tǒng),到編譯器,再到語(yǔ)言的定義與實(shí)現(xiàn)者,他們都在努力讓線(xiàn)程更好用,也有更少的限制。相反協(xié)程的現(xiàn)實(shí)要慘淡很多,在操作系統(tǒng)那里很少有協(xié)程的影子;只有Windows里,存在一個(gè)協(xié)程的實(shí)現(xiàn)——fiber(在那里它被稱(chēng)為纖程)。
從概念上來(lái)說(shuō),線(xiàn)程和協(xié)程最大的區(qū)別就是調(diào)度方式不同——線(xiàn)程是被動(dòng)調(diào)度的,協(xié)程是主動(dòng)調(diào)度的。也就是說(shuō),線(xiàn)程什么時(shí)候執(zhí)行,什么時(shí)候不執(zhí)行,完全是OS決定,線(xiàn)程是OS的菜,程序員最多只能主動(dòng)讓線(xiàn)程停下來(lái),卻很難讓線(xiàn)程主動(dòng)運(yùn)行起來(lái)。相反協(xié)程的執(zhí)行與停止完全由程序員決定,何時(shí)執(zhí)行、何時(shí)停止簡(jiǎn)單地調(diào)用一個(gè)協(xié)助方法就可以完成。因?yàn)榫€(xiàn)程是被動(dòng)調(diào)度的,所以線(xiàn)程需要大量同步方法;而協(xié)程是主動(dòng)調(diào)度的,所以協(xié)程之間的同步可以很自然完成——該停該跑——就這簡(jiǎn)單直接。
協(xié)程的概念很完美,使用可能也很方便。但是因?yàn)橐恍┪疫€不了解的原因,在C/C++里并沒(méi)有廣泛使用協(xié)程庫(kù)可用,這是因?yàn)楝F(xiàn)在的OS和編譯器無(wú)法很好地支持協(xié)程的實(shí)現(xiàn);這也許是歷史原因,線(xiàn)程在概念上與線(xiàn)程不相矛盾,但它的許多實(shí)現(xiàn)方式限制了實(shí)現(xiàn)通用安全的協(xié)程。在C語(yǔ)言和UNIX草莽朝代,實(shí)現(xiàn)一個(gè)協(xié)程要比實(shí)現(xiàn)一個(gè)函數(shù)(協(xié)程的一種特例)更困難,而對(duì)于當(dāng)時(shí)來(lái)說(shuō),函數(shù)已經(jīng)完全夠用了,如此協(xié)程被冷淡了。
在用戶(hù)層協(xié)程被死死地限制了,可是在linux內(nèi)核里,它卻一個(gè)基本的構(gòu)件——內(nèi)核線(xiàn)程,在更多的時(shí)候,更一個(gè)協(xié)程。當(dāng)然這句話(huà)是我自己說(shuō),在我很有限的Linux內(nèi)核經(jīng)驗(yàn)上說(shuō)的,極有可能是錯(cuò)誤的。
線(xiàn)程是操作系統(tǒng)實(shí)現(xiàn)的,也是操作系統(tǒng)的調(diào)度單位。我們知道,所謂調(diào)度就是操作系統(tǒng)根據(jù)一些規(guī)則決定在何時(shí)中止一個(gè)線(xiàn)程操作,并在一個(gè)恰當(dāng)?shù)臅r(shí)候再喚醒這個(gè)線(xiàn)程。這里有個(gè)自然的問(wèn)題,就是操作系統(tǒng)怎么實(shí)現(xiàn)中止與喚醒操作的?其時(shí)停止與喚醒操作都離不開(kāi)CPU的支持,CPU在硬件上實(shí)現(xiàn)了中斷機(jī)制,OS使用中斷機(jī)制實(shí)現(xiàn)了線(xiàn)程調(diào)度。OS可以告訴CPU,每過(guò)100ms(毫秒)或10ms你就調(diào)用一下一段代碼——這就是所謂的時(shí)鐘中斷例程,OS在這一小段代碼里實(shí)現(xiàn)了線(xiàn)程的中止與喚醒。在這段代碼里,OS通常會(huì)做這些事:
保存一下當(dāng)前的狀態(tài),當(dāng)前是哪個(gè)線(xiàn)程在執(zhí)行,當(dāng)前這個(gè)線(xiàn)程執(zhí)行到哪里了,等等——這些狀態(tài)實(shí)際上就是當(dāng)前CPU或CPU-Core里的數(shù)個(gè)特殊寄存器的數(shù)值。我們稱(chēng)這些狀態(tài)可能放在一個(gè)結(jié)構(gòu)體里,這個(gè)結(jié)構(gòu)體又被稱(chēng)為上下文(執(zhí)行上下文)。干一些其它的事,如更新系統(tǒng)時(shí)間,看看有沒(méi)有什么其它OS需要定時(shí)去做的事,等等。根據(jù)一些規(guī)則(也就是調(diào)度策略),在所有已經(jīng)中止的線(xiàn)程挑選一個(gè),把之前保存起來(lái)的狀態(tài)恢復(fù)——實(shí)際上就是把之前保存在結(jié)構(gòu)體的寄存器的值,再加載到相應(yīng)寄存器里。最后,OS執(zhí)行一個(gè)跳轉(zhuǎn)指令,把CPU的執(zhí)行權(quán)限傳遞給剛才挑選的線(xiàn)程,如此這個(gè)線(xiàn)程就從之前停下來(lái)的地方接著跑起來(lái)了。也就是說(shuō),它被喚醒了。在上面的說(shuō)明,我們看到一次保存狀態(tài)和一次恢復(fù)狀態(tài),所謂狀態(tài)就是上下文(狀態(tài)),而這個(gè)一次保存和一次恢復(fù)的過(guò)程稱(chēng)為一次上下文切換——大概意思就是說(shuō),剛才CPU在干這塊的活,現(xiàn)在它又切換到另一塊地方干活了。
現(xiàn)在我們知道上下文切換,以及OS是如何實(shí)現(xiàn)上下文切換的了,那么有一個(gè)問(wèn)題,就是程序員能不能在自己的程序里使用與OS相同功能的代碼,在自己的程序進(jìn)行上下文切換?答案是: 完全可以。事實(shí)上,這就是實(shí)現(xiàn)協(xié)程的基本方法。程序在執(zhí)行的時(shí)候是沒(méi)有函數(shù)這些概念的,諸如函數(shù)這些概念都上層語(yǔ)言及編譯器實(shí)現(xiàn)的抽象概念,CPU不認(rèn)識(shí)。CPU只認(rèn)識(shí)指令及指令操作的地址。
了解 C++ Boost 庫(kù)的人一定知道它最近新加了兩個(gè)庫(kù): context 和 coroutine —— 看這個(gè)名字就知道,前者是實(shí)現(xiàn)了上下文切換的,后者是利用前者實(shí)現(xiàn)了用戶(hù)態(tài)協(xié)程的。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注