接觸新概念,最好的辦法就是先總體看個大概,再回過頭來細細品味。前端
文中若是沒有特別說明,協程指編程語言級別的協程,線程則特指操做系統內核線程。編程
Kotlin 的協程從 v1.1 開始公測(Experimental) 到如今,已經算是很是成熟了,但你們對它的見解卻一直存在各類疑問,爲何呢?由於即使咱們把 Kotlin 丟掉,單純協程這個東西自己就已經長時間讓你們感到疑惑了,不信的話能夠單獨搜一下協程或者 Coroutine,甚至連 Lua 之父在提到爲何協程鮮見於早期語言實現,就是由於這概念沒有一個清晰的界定。緩存
更有意思的是,在查閱資料的過程當中,你會常常會陷入一種一下子『啊,我懂了』,一下子『啊,我懂個屁』的循環當中,不瞞各位說,我從七八年前剛開始學 Lua 的時候面對 Lua 的協程也是這個破感受,後來接觸 goroutine 又來了一遍,接觸 Kotlin 的協程又來了一遍,習慣就好。安全
) 那麼問題的關鍵在於,協程的概念是否是真的混亂呢?其實不是的,協程的概念最核心的點其實就是函數或者一段程序可以被掛起(說暫停其實也沒啥問題),待會兒再恢復,掛起和恢復是開發者的程序邏輯本身控制的,協程是經過主動掛起出讓運行權來實現協做的,就沒了,一句話就能說明白的事兒是否是特簡單?它跟線程最大的區別在於線程一旦開始執行,從任務的角度來看,就不會被暫停,直到任務結束這個過程都是連續的,線程之間是搶佔式的調度,所以也不存在協做問題。那麼咱們再來理一理協程的概念:微信
關鍵核心就是協程是一個能掛起而且待會兒恢復執行的東西。任什麼時候候本身產生疑惑的時候都回過來再想一想這幾句話,就算協程最終呈現給咱們的樣子可能『花裏胡哨』,但萬變不離其宗。多線程
有的朋友不理解什麼叫掛起,掛起這個詞其實還真是源於操做系統的叫法,直觀的理解上,你就當作暫停理解吧。併發
咱們前面提到,協程的概念其實並不混亂,那麼混亂的是什麼?是各家對它的實現。這就好像牛頓第二定律同樣,看似很簡單,F = ma,用起來就五花八門了,衍生的各類公式更是層出不窮。框架
協程不就是要掛起、恢復麼,請問掛起恢復具體要怎麼作?沒有定義呀。既然沒有定義是否是就能夠隨便?是的,抓住老鼠就是好貓~less
協程這一點兒跟線程真的是無法比啊,主流操做系統都有成熟的線程模型,應用層常常提到的線程的概念大多就是映射方式的差別,因此不一樣的編程語言一旦引入了線程,那麼基本上就是照搬了系統線程的概念,線程自己也不是他們實現的——這很好理解,由於線程調度是操做系統作的嘛。異步
Java 對線程作了很好的支持,這也是 Java 在高併發場景風生水起的一個關鍵支柱,不過若是你有興趣去看下虛擬機底層對線程的支持,例如 Android 虛擬機,其實就是 pthread。Java 的 Object 還有一個 wait 方法,這個方法幾乎支撐了各類鎖的實現,它底層是 condition。
絕大多數協程都是語言層面本身的實現,不一樣的編程語言有不一樣的使用場景,天然在實現上也看似有很大的差別,甚至還有的語言本身沒有實現協程,但開發者經過第三方框架的方式提供了協程的能力,例如 Java 的框架 Quasar(docs.paralleluniverse.co/quasar/),加上協程實現自己在操做系統層面就有過一系列演進,所以出現了雖然理論上看起來很簡單,但實現上卻多樣化的局面。
咱們在前面講各個語言的實現有差別,說的是看似有很大的差別,主要是各自的關鍵字、類型命名不同,但總結下來你們對於協程的分類更傾向於按照有沒有棧來分,即:
棧這個東西你們應該都很熟悉了,咱們遞歸調用函數的層次太多就會致使 StackOverflowException
,由於棧內存是有限的;咱們的程序出現了異常咱們老是但願看到異常點的調用關係,這樣方便定位問題,這也須要棧。
有棧協程有什麼好處呢?由於有棧,因此在任何一個調用的地方運行時均可以選擇把棧保存起來,暫停這個協程,聽起來就跟線程同樣了,只不過掛起和恢復執行的權限在程序本身,而不是操做系統。缺點也是很是明顯的,每建立一個協程無論有沒有在運行都要爲它開闢一個棧,這也是目前無棧協程流行的緣由。
goroutine 看上去彷佛不像協程,由於開發者本身沒法決定一個協程的掛起和恢復,這個工做是 go 運行時本身處理的。爲了支持 goroutine 在任意位置能掛起,goroutine 實際上是一個有棧協程,go 運行時在這裏作了大量的優化,它的棧內存能夠根據須要進行擴容和縮容,最小通常爲內存頁長 4KB。
JavaScript、C# 還有 Python 的協程,或者乾脆就說 async/await,相比之下就輕量多了,它們看起來更像是針對回調加了個語法糖的支持——它們其實就是無棧協程的實現了。無棧,顧名思義,每個協程都不會單獨開闢調用棧,那麼問題來了,它的上下文是如何保存的?
這就要提到傳說中的 CPS 了,即 continuation-passing-style。咱們來想象一下,程序被掛起,或者說中斷,最關鍵的是什麼?是保存掛起點,或者中斷點,對於線程被操做系統中斷,中斷點就是被保存在調用棧當中的,而咱們的無棧協程要保存到哪兒呢?保存到 Continuation 對象當中,這個東西可能在不一樣的語言當中叫法不同,但本質上都是一個 Continuation,它就是一個普通的對象,佔用內存很是小,仍是很抽象是吧,想一想你常見的 Callback,它其實就是一個 Continuation 的實現。
Kotlin 的協程的根基就是一個叫作 Continuation 的類。我在前面的文章不止一次提到,這傢伙長得橫看豎看就是一個回調,resume 就是 onSuccess,resumeWithException 就是 onFailure。
Continuation 攜帶了協程繼續執行所須要的上下文,同時它本身又是掛起點,由於待會兒恢復執行的時候只須要執行它回調的函數體就能夠了。對於 Kotlin 來說,每個 suspend
函數都是一個掛起點,意味着對於當前協程來講,每遇到一個 suspend
函數的調用,它都有可能會被掛起。每個 suspend
函數都被編譯器插入了一個 Continuation 類型的參數用來保存當前的調用點:
suspend fun hello() = suspendCoroutine<Int>{ continuation ->
println("Hello")
continuation.resumeWith(Result.success(10086))
}
複製代碼
咱們定義了一個 suspend
函數 hello
,它看起來沒有接收任何參數,若是真是這樣,請問咱們在後面調用 resumeWith
的 continuation
是哪裏來的?
都說掛起函數必須在協程內部調用,其實也不是,咱們在前面講掛起原理的時候就用 Java 代碼直接去調用 suspend
函數,你們也會發現這些 suspend
函數都須要傳入一個額外的 Continuation
,就是這個意思。
固然,Java 也不是必須的,咱們只須要用點兒 Kotlin 反射,同樣能夠直接讓 suspend 函數現出原形:
val helloRef = ::hello
val result = helloRef.call(object: Continuation<Int>{
override val context = EmptyCoroutineContext
override fun resumeWith(result: Result<Int>{
pritnln("resumeWith: ${result.getOrNull()}")
})
})
複製代碼
這與咱們在協程掛起原理那篇的作法一模一樣,咱們雖然沒有辦法直接調用 hello()
,但咱們能夠拿到它的函數引用,用發射調用它(這個作法後續可能也會被禁掉,但 1.3.50 目前仍然是可用的),調用的時候若是你什麼參數都不傳,編譯器就會提示你它須要一個參數,呃,你看,它這麼快就投降了——須要的這個參數正是 Continuation
。
再強調一下,這段代碼不須要運行在協程體內,或者其餘的 suspend
函數中。如今請你們仔細想一想,爲何官方要求 suspend
函數必定要運行在協程體內或者其餘 suspend
函數中呢?
答案天然就是任何一個協程體或者 suspend
函數中都有一個隱含的 Continuation
實例,編譯器可以對這個實例進行正確傳遞,並將這個細節隱藏在協程的背後,讓咱們的異步代碼看起來像同步代碼同樣。
說到這裏,咱們已經接近 Kotlin 協程的本質了,它是一種無棧協程實現,它的本質就是一段代碼 + Continuation 實例。
這個說法實際上是很奇怪的。我若是問你線程實際上是一個 CPU 框架嗎,你確定會以爲這倆,啥啊???
Kotlin 協程確實在實現的過程當中提供了切線程的能力,這是它的能力,不是它的身份,就比如拿着學位證非說這是身份證同樣,學位證描述的是這人能幹啥,不能描述這人是誰。
槓精們可能會說學位證有照片有名字啊。你拿着學位證去買飛機票你看人家認不認唄。
協程的世界能夠沒有線程,若是操做系統的 CPU 調度模型是協程的話;反過來也成立——這個應該不會有人反對吧。Kotlin 協程是否是能夠沒有線程呢?至少從 Java 虛擬機的實現上來看,好像。。。。不太行啊。沒錯,是不太行,不過這不是 Kotlin 協程的問題,是 Java 虛擬機的問題,誰讓 Java 虛擬機的線程用起來沒有那麼難用呢,在它剛出來的時候簡直吊打了當時其餘語言對併發的支持(就像 goroutine 出來的時候吊打它同樣)。
咱們知道 Kotlin 除了支持 Java 虛擬機以外,還支持 JavaScript,還支持 Native。JavaScript 不管是跑在 Web 仍是 Node.js 當中,都是單線程玩耍的;Kotlin Native 雖然能夠調用 pthread,但官方表示咱們有本身的併發模型(Worker),不建議直接使用線程。在這兩個平臺上跑,Kotlin 的協程其實都是單線程的,又怎麼講是個線程框架呢?
說到這兒可能又有人有疑問了,單線程要協程能作什麼呢?這個前端同窗可能會比較有感觸,誰跟大家說的異步必定要多線程。。Android 開發的同窗其實能夠想一想你在 Activity
剛建立的時候想要拿到一個 View 的大小通常返回都是 0,由於 Activity
的佈局是在 onResume
方法調用以後完成的,因此 handler.post
一下就行了:
override fun onResume(){
super.onResume()
handler.post{
val width = myView.width
...
}
}
複製代碼
這就是異步代碼嘛,但這代碼其實都運行在主線程的,咱們固然能夠用協程改寫一下:
override fun onResume() {
super.onReusme()
GlobalScop.launch(Dispatchers.Main) {
val width = hadler.postSuspend {
myView.width
}
Log.d("MyView",widht.toString())
}
}
suspend fun <T> Handler.postSuspend(block: () -< T) = suspendCoroutine<T> {
post {
it.resume(block())
}
}
複製代碼
其實我我的以爲若是 Kotlin 協程的默認的調度器是 Main
,而且這個 Main
會根據各自平臺選擇一個合適的事件循環,這樣更能體現 Kotlin 協程在不一樣平臺的一致性,例如對於 Android 來講 Main
就是 UI 線程上的事件循環,對於 Swing 一樣是 Swing 的 UI 事件循環,只要是有事件循環的平臺就默認基於這個循環來一個調度器,沒有默認事件循環的也好辦,Kotlin 協程自己就有 runBlocking
嘛,對於普通 Java 程序來講沒有事件循環就給它構造一個就好了。
Kotlin 協程的設計者沒有這樣作,他們固然也有他們的道理,畢竟他們不肯意強迫開發者必定要用協程,甚至馬上立刻就得對原有的代碼進行改造,他們但願 Kotlin 只是一門編程語言,一門提供足夠安全保障和靈活語法的編程語言,剩下的交給開發者去選擇。
這可不是一個很容易回答的問題。
Kotlin 協程剛出來的時候,有人就作過性能對比,以爲協程沒有任何性能優點。咱們徹底能夠認爲他的測試方法是專業的,在一些場景確實用協程不會有任何性能上的優點,這就比如咱們須要在一個單核 CPU 上跑一個計算密集型的程序還要開多個線程跑同樣,任何特性都有適合它的場景和不適合它的領域。
想必你們看各種講解協程的文章都會提到協程比線程輕量,這個其實咱們前面也解釋過了,編程語言級別實現的協程就是程序內部的邏輯,不會涉及操做系統的資源之間的切換,操做系統的內核線程天然會重一些,且不說每建立一個線程就會開闢的棧帶來的內存開銷,線程在上下文切換的時候須要 CPU 把高速緩存清掉並從內存中替換下一個線程的內存數據,而且處理上一個內存的中斷點保存就是一個開銷很大的事兒。若是沒有直觀的感覺的話,就盡情想象一下你正要拿五殺的時候公司領導在微信羣裏發消息問你今天的活躍怎麼跌了的場景。
線程除了包含內核線程自己執行代碼能力的含義之外,一般也被賦予了邏輯任務的概念,因此協程是一種輕量級的『線程』的說法,更多描述的是它的使用場景,這句話也許這樣說更貼切一些:
協程更像一種輕量級的『線程』。
線程天然能夠享受到並行計算的優待,協程則只能依賴程序內部的線程來實現並行計算。協程的優點其實更可能是體如今 IO 密集型程序上,這對於 Java 開發者來講可能又是一個很迷惑的事情,由於你們寫 Java 這麼多年,不多有人用上 NIO,絕大多數都是用 BIO 來讀寫 IO,所以無論開線程仍是開協程,讀寫 IO 的時候老是要有一個線程在等待 IO,因此看上去彷佛也沒有什麼區別。但用 NIO 就不同了,IO 不阻塞,經過開一個或不多的幾個線程來 select IO 的事件,有 IO 事件到達時再分配相應的線程去讀寫 IO,比起傳統的 IO 就已經有了很大的提高。
欸?沒有寫錯嗎?你寫的但是線程啊?
對啊,用了 NIO 之後,自己就能夠減小線程的使用,沒錯的。但是協程呢?協程能夠基於這個思路進一步簡化代碼的組織,雖然線程就能解決問題,但寫起來實際上是很累的,協程可讓你更輕鬆,特別是遇到多個任務須要訪問公共資源時,若是每一個任務都分配一個線程去處理,那麼少不了就有線程會花費大量的時間在等待獲取鎖上,但若是咱們用協程來承載任務,用極少許的線程來承載協程,那麼鎖優化就變得簡單了:協程若是沒法獲取到鎖,那麼協程掛起,對應的線程就可讓出去運行其餘協程了。
我更願意把協程做爲更貼近業務邏輯甚至人類思考層面的一種抽象,這個抽象層次其實已經比線程更高了。線程可讓咱們的程序併發的跑,協程可讓併發程序跑得看起來更美好。
線程自己就能夠,爲何要用協程呢?這就像咱們常常被人問起 Java 就能夠解決問題,我爲何要用 Kotlin 呢?爲何你說呢?
總的來講,無論是異步代碼同步化,仍是併發代碼簡潔化,協程的出現實際上是爲代碼從計算機向人類思惟的貼近提供了可能。
歡迎關注 Kotlin 中文社區!
中文官網:www.kotlincn.net/
中文官方博客:www.kotliner.cn/
公衆號:Kotlin
知乎專欄:Kotlin
CSDN:Kotlin中文社區
掘金:Kotlin中文社區
簡書:Kotlin中文社區