Go 面試官:什么是協(xié)程,協(xié)程和線程的區(qū)別和聯(lián)系?
大家好,我是煎魚。
最近金三銀四,是面試的季節(jié)。在我的 Go 讀者交流群里出現(xiàn)了許多小伙伴在討論自己面試過(guò)程中所遇到的一些 Go 面試題。
今天的男主角,是工程師的必修技能,那就是 “什么是協(xié)程,協(xié)程和線程的區(qū)別和聯(lián)系?”
既要理解線程,還要講解協(xié)程,并且詮釋兩者間的區(qū)別,但是由于提到線程,就必然涉及進(jìn)程,因此本文將會(huì)同時(shí)梳理介紹 “進(jìn)程、協(xié)程、協(xié)程” 三者的隨筆知識(shí),希望能引發(fā)大家的一些思考。
吸魚之路開始。
進(jìn)程
進(jìn)程是什么
進(jìn)程是操作系統(tǒng)對(duì)一個(gè)正在運(yùn)行的程序的一種抽象,進(jìn)程是資源分配的最小單位。
進(jìn)程在操作系統(tǒng)中的抽象表現(xiàn)
為什么有進(jìn)程
為什么會(huì)有 ”進(jìn)程“ 呢?說(shuō)白了還是為了合理壓榨 CPU 的性能和分配運(yùn)行的時(shí)間片,不能 “閑著“。
在計(jì)算機(jī)中,其計(jì)算核心是 CPU,負(fù)責(zé)所有計(jì)算相關(guān)的工作和資源。單個(gè) CPU 一次只能運(yùn)行一個(gè)任務(wù)。如果一個(gè)進(jìn)程跑著,就把唯一一個(gè) CPU 給完全占住,那是非常不合理的。
那為什么要壓榨 CPU 的性能?因?yàn)?CPU 實(shí)在是太快,太快,太快了,寄存器僅僅能夠追的上他的腳步,RAM 和別的掛在各總線上的設(shè)備則更是望塵莫及。
多進(jìn)程的緣由
如果總是在運(yùn)行一個(gè)進(jìn)程上的任務(wù),就會(huì)出現(xiàn)一個(gè)現(xiàn)象。就是任務(wù)不一定總是在執(zhí)行 ”計(jì)算型“ 的任務(wù),會(huì)有很大可能是在執(zhí)行網(wǎng)絡(luò)調(diào)用,阻塞了,CPU 豈不就浪費(fèi)了?
進(jìn)程的上下文切換
這又出現(xiàn)了多進(jìn)程,多個(gè) CPU,多個(gè)進(jìn)程。多進(jìn)程就是指計(jì)算機(jī)系統(tǒng)可以同時(shí)執(zhí)行多個(gè)進(jìn)程,從一個(gè)進(jìn)程到另外一個(gè)進(jìn)程的轉(zhuǎn)換是由操作系統(tǒng)內(nèi)核管理的,一般是同時(shí)運(yùn)行多個(gè)軟件。
線程
有了多進(jìn)程,想必在操作系統(tǒng)上可以同時(shí)運(yùn)行多個(gè)進(jìn)程。那么為什么有了進(jìn)程,還要線程呢?
原因如下:
進(jìn)程間的信息難以共享數(shù)據(jù),父子進(jìn)程并未共享內(nèi)存,需要通過(guò)進(jìn)程間通信(IPC),在進(jìn)程間進(jìn)行信息交換,性能開銷較大。
創(chuàng)建進(jìn)程(一般是調(diào)用 fork 方法)的性能開銷較大。
大家又把目光轉(zhuǎn)向了進(jìn)程內(nèi),能不能在進(jìn)程里做點(diǎn)什么呢?
進(jìn)程由多個(gè)線程組成
一個(gè)進(jìn)程可以由多個(gè)稱為線程的執(zhí)行單元組成。每個(gè)線程都運(yùn)行在進(jìn)程的上下文中,共享著同樣的代碼和全局?jǐn)?shù)據(jù)。
多個(gè)進(jìn)程,就可以有更多的線程。多線程比多進(jìn)程之間更容易共享數(shù)據(jù),在上下文切換中線程一般比進(jìn)程更高效。
原因如下:
- 線程之間能夠非常方便、快速地共享數(shù)據(jù)。
- 只需將數(shù)據(jù)復(fù)制到進(jìn)程中的共享區(qū)域就可以了,但需要注意避免多個(gè)線程修改同一份內(nèi)存。
- 創(chuàng)建線程比創(chuàng)建進(jìn)程要快 10 倍甚至更多。
- 線程都是同一個(gè)進(jìn)程下自家的孩子,像是內(nèi)存頁(yè)、頁(yè)表等就不需要了。
協(xié)程是怎么回事
協(xié)程是什么
協(xié)程(Coroutine)是用戶態(tài)的線程。通常創(chuàng)建協(xié)程時(shí),會(huì)從進(jìn)程的堆中分配一段內(nèi)存作為協(xié)程的棧。
線程的棧有 8 MB,而協(xié)程棧的大小通常只有 KB,而 Go 語(yǔ)言的協(xié)程更夸張,只有 2-4KB,非常的輕巧。
協(xié)程的誕生
根據(jù)維基百科的說(shuō)法,馬爾文·康威于 1958 年發(fā)明了術(shù)語(yǔ) “coroutine” 并用于構(gòu)建匯編程序,關(guān)于協(xié)程最初的出版解說(shuō)在 1963 年發(fā)表。
也就是歷史上是先有的 “協(xié)程”,再有的 “線程”,線程是在在協(xié)程的基礎(chǔ)上添加了棧等功能后擴(kuò)展出來(lái)的。
但為什么一開始協(xié)程沒有火起來(lái)呢?這個(gè)比較難考證,大概率還是與 60 年前的計(jì)算機(jī)時(shí)代背景有關(guān)。
而如今人們把協(xié)程調(diào)度的邏輯更進(jìn)一步抽象為 “等 IO,讓出,IO 完畢”,在此基礎(chǔ)上人們發(fā)現(xiàn)協(xié)程的方式能解決多線程環(huán)境下很多代碼邏輯 “混亂”。
協(xié)程的優(yōu)勢(shì)
既然線程似乎已經(jīng)很好地填補(bǔ)了進(jìn)程的遺憾,那怎么又出來(lái)了一個(gè) “協(xié)程”,難道是重復(fù)造輪子嗎?
協(xié)程的優(yōu)勢(shì)(via InfoQ @八兩)如下:
- 節(jié)省 CPU:避免系統(tǒng)內(nèi)核級(jí)的線程頻繁切換,造成的 CPU 資源浪費(fèi)。好鋼用在刀刃上。而協(xié)程是用戶態(tài)的線程,用戶可以自行控制協(xié)程的創(chuàng)建于銷毀,極大程度避免了系統(tǒng)級(jí)線程上下文切換造成的資源浪費(fèi)。
- 節(jié)約內(nèi)存:在 64 位的Linux中,一個(gè)線程需要分配 8MB 棧內(nèi)存和 64MB 堆內(nèi)存,系統(tǒng)內(nèi)存的制約導(dǎo)致我們無(wú)法開啟更多線程實(shí)現(xiàn)高并發(fā)。而在協(xié)程編程模式下,可以輕松有十幾萬(wàn)協(xié)程,這是線程無(wú)法比擬的。
- 穩(wěn)定性:前面提到線程之間通過(guò)內(nèi)存來(lái)共享數(shù)據(jù),這也導(dǎo)致了一個(gè)問題,任何一個(gè)線程出錯(cuò)時(shí),進(jìn)程中的所有線程都會(huì)跟著一起崩潰。
- 開發(fā)效率:使用協(xié)程在開發(fā)程序之中,可以很方便的將一些耗時(shí)的IO操作異步化,例如寫文件、耗時(shí) IO 請(qǐng)求等。
協(xié)程本質(zhì)上就是用戶態(tài)下的線程,所以也有人說(shuō)協(xié)程是 “輕線程”,但我們一定要區(qū)分用戶態(tài)和內(nèi)核態(tài)的區(qū)別,很關(guān)鍵。
總結(jié)
歸歸根到底,在日?;蛎嬖囍杏龅?“什么是協(xié)程,協(xié)程和線程的區(qū)別和聯(lián)系?” 這類問題時(shí),面試者常規(guī)會(huì)把進(jìn)程、線程、協(xié)程都介紹一遍。
為了方便記憶和詮釋,推薦大家結(jié)合故事來(lái)講會(huì)比較好,這一塊可以參考阮一峰大神翻譯的《進(jìn)程與線程的一個(gè)簡(jiǎn)單解釋》,會(huì)帶來(lái)不少好感。
而最關(guān)鍵的部分,在于協(xié)程和線程的區(qū)別和聯(lián)系是什么?
我們可以通過(guò)文章中的介紹,從協(xié)程 -> 線程的歷史進(jìn)程來(lái)說(shuō)明。接著進(jìn)一步對(duì)比協(xié)程和線程兩者的優(yōu)勢(shì)和缺點(diǎn),就能比較好的詮釋區(qū)別和聯(lián)系了。
更優(yōu)秀的部分,可以詮釋完基本概念和區(qū)別后,進(jìn)一步延伸都你所面試的崗位,例如是 Go 語(yǔ)言,就可以介紹 Go 語(yǔ)言的協(xié)程的具體應(yīng)用和實(shí)現(xiàn)。
畢竟,Go 語(yǔ)言可以輕輕松松開數(shù)十萬(wàn)個(gè)協(xié)程,毫無(wú)波瀾。這樣能夠更好的體現(xiàn)你對(duì)協(xié)程、線程的知識(shí)深度和廣度應(yīng)用,而不是單純的背概念。
參考
線程和進(jìn)程的區(qū)別是什么?
有了多線程,為什么還要有協(xié)程?
進(jìn)程與線程的一個(gè)簡(jiǎn)單解釋