聊聊協程的發展歷程

前言

本文講的協程主要以kotlin爲主,同時可能參考python,go,可是會盡可能避免使用代碼,而是嘗試用通俗的語言來聊協程的發展歷程,儘可能保證你們都能理解。python

近些年,一些編程語言的新貴Go和Kotlin紛紛引入了協程這個語言特性,使得協程這個彷佛十分陌生的概念開始頻繁進入你們的視野,爲了便於理解,開發者們都把它看成線程的小弟來對待,即輕量級線程。但是真要細提及來,協程實際上是很早就出現的一個編程概念,它的出現甚至是是早於線程的,可是就編程語言的江湖地位而言,協程是不如線程的,因此向線程低頭叫爸爸不奇怪。編程

看了我上面的介紹,你們必定很納悶,你說協程出現早,有資歷,那爲啥幾十年的編程語言發展下來就就混成了這副鹹魚樣?線程出現晚,但怎麼就一棵星星之火點着了編程語言的草原。成爲了編程語言中的重要概念呢??再者,協程幾十年的鹹魚一條,到現在怎麼忽然有了夢想,翻身把歌唱的呢?後端

今天就和你們一塊兒來梳理一下協程的整個發展歷程,但願能幫助你們更加理解協程。promise

協程的出現

我們先來講說協程的歷史,以及它是怎麼混的這麼慘的,畢竟悲催的人生都須要一個解釋。bash

協程最先誕生於1958年,被應用於彙編語言中(距今已有60多年了),對它的完整定義發表於1963 年,協程是一種經過代碼執行的恢復與暫停來實現協做式的多任務的程序組件多線程

而與此同時,線程的出現則要晚一些,伴隨着操做系統的出現,線程大概在1967年被提出。線程做爲由操做系統調度最小執行組件,主要用於實現搶佔式的多任務。異步

既然你們都搞多任務,按說誰也不能比誰強多少啊,何況協程還早生幾年,理論上經過自身努力發展,在編程語言中佔據核心地位是極有可能的。async

可是這協程的發展啊,一方面固然要靠自我奮鬥,另外一方面,也要考慮歷史進程。而上個世紀七八九十年代,是計算機瘋狂朝着小型化和我的化的方向演進的時代,計算機很是依賴操做系統來提供用戶交互和壓榨CPU的最大性能,而操做系統怎麼來壓榨計算機性能的呢?靠多線程。操做系統跟隨我的計算機的普及以後,編程語言天然也開始依賴操做系統提供的接口來駕馭計算機了,線程成了幾乎全部編程語言跳不過的一個重要概念,並一直延續至今。編程語言

到這裏你可能要問了,你們都是搞多任務的,爲何線程能提高cpu的資源利用率,協程不能呢?異步編程

固然有不少其餘的緣由能解釋,但最本質的緣由仍然是協程和線程是有顯著區別的兩個概念,到這裏咱們就要回過頭來聊聊什麼叫協做式多任務,什麼叫搶佔式多任務?以及這兩種任務是不是同一種概念?

  • 協做式多任務:

上圖是一個壽司生產的部分工序,咱們能夠把圖中的傳送轉盤和機器抓手可視做兩個任務,一塊兒協做完成了食物的生產。這就是協做式多任務。協做式的多任務要求任務之間相互熟悉,才能實現協做。

  • 搶佔式多任務:

喂金魚的場景,一把飼料下去,全部金魚立刻圍上來一搶而空,這裏每一個金魚都至關於一個任務線程,這就是搶佔式多任務。而搶佔式多任務(線程)之間不須要了解和配合,只有競爭關係。

上面兩張圖,比較生動的展現了協做式多任務(協程)和搶佔式多任務(多線程)之間的區別。咱們可以發現,協程更加適合哪些相互熟悉的任務組件經過密切配合協做完成某些工做,協做式多任務裏的「任務」是一種子程序(可稱爲函數)。搶佔式多任務裏的任務則是指能搶佔資源的組件或代碼(其實就是線程),這裏的多任務也就是多線程。因此說,協程和線程原本是差別很是大的兩種概念,他們的能力是不一樣的,而線程的這種能力正好迎合了那個時代的需求。自我奮鬥+歷史進程是線程成功的主要緣由。

固然,在另外一方面,也因爲協程是基於編程語言層面的一種概念,它並無統必定義的接口,所以在不一樣的語言中實現後的效果是不一樣的,這也會對開發者形成極大的困擾,不利於它的推廣。而反觀線程,經過操做系統的統一接口,定義了大致相同的線程使用方式,保證了不一樣的編程語言都對線程的使用是大致一致。

講到這裏,咱們來總結下協程早期發展不順的緣由

  • 1,協程沒有表明先進生產力的發展要求,先進文化的前進方向,和最廣開發者的根本利益[手動狗頭]。
  • 2,協程在不一樣編程語言中,它的實際表現有差別,很是不利於開發者的理解和使用。

以上兩點,就是協程幾十年以來一直不溫不火的緣由。咱們也看到,雖然看起來都在搞多任務,可是協程和線程實際是沒有太多交集的。

鹹魚翻身

雖然說協程這種協做式多任務的組件不能提升程序執行的效率,彷佛沒有太普遍的應用前景,但這協程吶,也不能隨意否認本身,由於不知道何時,你就忽然被歷史進程給關照了。

仍是從線程提及,雖然線程成爲編程世界的重要概念,可是在多年的使用過程當中開發者們也逐漸意識到了它的痛點:

  • 線程之間(異步代碼)難以交互難度比較大,每每只能用callback,大量的callback會代碼難以閱讀和理解,最終讓項目變得難以維護。

簡單說就是在開發者端,線程之間如何更方便的交互

而這裏協程能作什麼呢?

或許我再從新表達一下線程的痛點:在開發者端,線程之間如何更方便的協做

回想一下,咱們是怎麼介紹協程的?協做式多任務對吧,還記得上圖中的轉盤和機器抓手的協做麼?咱們當時說這兩個任務更像是兩個函數的協做,但若是把轉盤和機器抓手視做兩個線程呢?藉助編譯器,把線程封裝成一個個能暫停和恢復的函數,線程是否是就能夠像協程設計的那樣協做呢?

咱們仍是從代碼層面來看看現在協程是如何被使用的吧。設計一個簡單的需求:社區內用戶進行發帖時,須要先從後臺驗證發帖權限,請求兩個接口,那麼可能咱們須要嘗試開啓兩個線程前後來完成。

  • 普通的callback代碼:
fun tryPost(){
    // 先經過接口驗證權限 (實際開啓了一個線程,而後等待回調)
    findUserPermission(user,callback()){
        public void onSuccess(UserPermission response){
            // 回調 若是成功 ,檢查是否有權限
            if(response.hasPermission){
            // 若是有權限,則訪問發帖接口 (一樣開啓線程,等待回調)
                postContent(content,callback()){ 
                    public void onSuccess(Result response){
                        // handle successful response
                    }
                    public void onFail(){
                        
                    }
                }
            }else{
                // 若是無權限,則......
            }
        }
        
        public void onFail(){
            
        }
    }
}
複製代碼

這是比較常見的作法,咱們須要訪問兩次接口,而交互只能在callback中進行,可是其實代碼已經很難看了,若是還有其餘的邏輯的話,那代碼只會更加冗雜,難以維護。

那協程是怎麼解決這種痛點的呢?咱們看看(kotlin和python)協程的代碼如何實現這種需求:

  • kotlin的協程代碼
// 函數經過suspend關鍵字標識,能夠被協程調用,具有暫停恢復的能力 ,實際上仍然使用了io線程來完成接口請求
suspend fun tryfindUserPermission():PermissionResponse {
    return withContext(Dispatchers.IO){
                findUserPermission(user)
        }
}

// 函數經過suspend關鍵字標識,能夠被協程調用
suspend fun post():Result {
    return  withContext(Dispatchers.IO) {
                postContent(content)
            }
}
    
fun tryPost(){
    //啓動一個協程
     launch{
     //代碼執行到這一行,讓出cpu,進入暫停狀態,等待請求成功以後,會恢復執行)
        var response = tryfindUserPermission()    // 向後端訪問用戶權限 
        if(response.hasPermission){
        // 有權限則開始發帖 (開啓線程,讓出cpu,暫停執行,等待恢復)
            var response =  post()
             // handle response if need
        }
     }
 }
複製代碼

能夠看到,在kotlin中,協程經過把線程裏的代碼封裝成一種能暫停/恢復的函數,讓多線程之間的交互就像普通的函數同樣簡單,不須要callback。

  • python的協程代碼
import asyncio

// async 的關鍵字,代表這個函數能夠被協程調用
async def findUserPermission():
    // handle http request
    ...
    ...
    return response
async def postContent():
    // handle http request
    ...
    ...
    return response
    
    
async def main():
// 嘗試獲取權限信息 一樣會讓出cpu,進入暫停狀態,等待恢復
    reponse = await findUserPermission()
    // 判斷有權限的狀況下,進行發帖
    if response.hasPermission :
        res = await postContent()
        // handle response if need

asyncio.run(main())

複製代碼

python經過協程處理這種問題本質和kotlin是一致的。

相信你們也能看到,協程在不一樣的語言中的表現方式是有差別的

經過上面幾段僞代碼,咱們可以比較清楚看到,協程能很是明顯的簡化了線程之間協做複雜度,讓咱們能夠以編寫同步代碼的方式來編寫異步代碼,極大的簡化你的邏輯,讓你的代碼容易維護。

那麼協程是如何作到的呢?

雖然不一樣的語言中,協程有所差別,可是原理都差很少,編程語言的編譯器經過一些關鍵字(kotlin中用suspend,python中用async等)來修飾函數,在編譯期間根據關鍵字生成一些線程相關的代碼來實現函數的暫停恢復的功能,從而實現把線程相關的代碼留在編譯期間產生,在開發層面就能提供像普通函數通常的協做方式。

由於解決了這個痛點,協程開始變得愈來愈受開發者歡迎。而協程經過編譯器的幫助把線程相關的代碼留在了編譯期間產生,開發這能夠經過操做協程就能夠達到使用線程的目的,因此如今你們認爲協程是一種輕量級的線程。

對於多線程的協做,或者說異步代碼之間的協做並非只有協程一家解決方案,在JS中,有promise,Java中有RxJava等等,他們都致力於解決異步編程的相關問題,但願能以編寫同步代碼的方式來寫異步代碼,目前來看,他們都作的很不錯。

總結

你們對於協程的理解有不少分歧,可是對我而言,協程其實得分兩個階段來理解

  • 在協程誕生之初,只是用來解決編程中的某些特殊問題的編程組件,它的多任務更像多個函數的組合協做執行,那個時候,協程其實更像是一種具有暫停恢復的函數。可是這種功能彷佛並不受歡迎,所以協程在很長一段時間內都是比較小衆的。(此時協程和線程關係並不大)
  • 現在它成爲底層支持多線程的協做式多任務組件,很好的解決了線程協做的痛點,同時也逐漸變得愈來愈受歡迎,協程和線程的關係更加親密,它們彷佛也變得更加類似。(現在你能夠把協程視做一種輕量級線程)

而協程的發展歷程,其實也就是經歷了這兩個階段。

相關文章
相關標籤/搜索