Kotlin協程淺談

進程、線程、協程是啥?網上有不少生動形象的例子,我我的認爲,搞這麼多花裏胡哨的,目的就一個:最大化利用資源。java

注:進程、線程不是我想寫的重點,協程纔是,但不提一下前面兩個,感受就像吃🍔🍟沒有🥤同樣奇怪。android

令打工人悲傷的故事

本文廢話較多,不喜勿噴,耐心看下去,說不定會有意想不到的收穫git

進程

對於進程而言,合理利用的資源在本文中是指cpu的時間資源。程序員

完成一件事情須要不少步驟,執行步驟有不少策略,假設你的策略是one by one:必須一個步驟作完再進行下一步。github

若是cpu也像你同樣的策略,那在對寄存器,內存,硬盤進行IO操做時,cpu就躺着休息了!由於cpu太快,很是快,而寄存器比較慢,內存更慢,硬盤更更慢,因此cpu只能等數據傳輸完,時間就浪費在了這裏。編程

爲了合理利用cpu等待的這段時間。可讓cpu先提早作其餘步驟。(打工人寫到這裏,眼眶都溼潤了)api

image

那就把各個步驟,放到不一樣的進程去執行,cpu在執行一個步驟,遇到了耗時等待的io時,就去執行另外一個步驟,也就是執行另外一個進程,但這時又發現一個問題,cpu確實把等待的時間利用起來了,可是有一部分時間並無執行進程裏面的代碼,而是在作進程環境的切換。markdown

舉個不恰當的例子:多線程

你去找張三,讓張三幫你寫個網頁,但張三說你提的這個功能技術上須要調研一下 (IO耗時操做),讓你等他20分鐘。你想,我先去找李四吧,先讓李四把後臺的協議先定下來,從張三到李四家花了5分鐘,和李四的討論花了10分鐘。而後你回去找張三,到張三家裏時,張三正好調研完了。閉包

20分鐘沒有在張三家裏白等,可是你花費了10分鐘在張三到李四家往返,10分鐘你均可以讓王五再改幾版稿子,趙六再提幾個場景了,老子分分鐘幾十萬上下,這張三和李四就不能住一塊兒嗎?在這裏,張三和李四的房子都是屬於進程,進程間的切換稍微耗費點時間。

線程

你以爲時間都浪費在沒有價值的地方,因此你就租了棟樓,讓張三李四住一塊兒,一人一層,這樣找他們的時候就簡單了!對了,把王五趙六也叫上吧。在這裏,咱們把這棟樓看作進程,張三和李四看作線程。這個時候你找張三找李四都快多了。(打工人寫到這裏,眼淚已經流下來了)

image

隨着業務越作越大,愈來愈多的應酬,你以爲手下的人真煩,每一個人搶着都找你嘰嘰喳喳(一些操做系統的線程執行是搶佔式的),管理時間成爲了你的首要問題。因而你請了個祕書,會議的事情讓祕書(操做系統)安排一下,每一個人最多隻有兩分鐘的時間,固然,級別高的能夠直接找你,超時也容許(一些操做系統是優先級高的能夠是搶佔式的,而普通線程是非強佔式的)。

在祕書的安排下(調度),張三李四們就按祕書的安排來你的辦公室,和你開會,一次的時間也就那麼長,談不完就下次再談。但後來你發現,下一個到張三,但張三從工位到你辦公室也要一點點時間。你怎麼能忍受這些時間的浪費?這個例子可能舉的很差,實際上是資源競爭和線程上下文切換的問題,多個線程在執行時,必然會對同一個資源進行競爭。這就像多個員工去競爭你這個老闆同樣,因此須要對共享資源進行加鎖,而加鎖就會涉及到線程狀態的切換,這裏是會浪費時間的。同時線程切換也浪費了一丟丟時間。

協程

因而你引進了一套在線會議系統,你只須要按照祕書的安排,和對應的人開會就好,這樣就減小了他們過來你辦公室的時間。祕書通知某個員工到他了,再讓其餘全部員工等待。這也浪費了一點時間。

那若是容許員工之間提早溝通時間,由於他們知道本身何時有時間,他們能夠主動讓出這個開會的時間給其餘人,並提早定好要開會的時間,一到時間直接開會,省去了祕書的通知,時間又節省了一點。這就是協程的主動讓出cpu全部權的行爲。固然,因爲在線會議的引入,雖然你和一萬個員工在開會,但對你而言,你所面對都是同一個屏幕。這裏類比的可能也不恰當,但要知道的是,有些時候無論有多少個協程,對cpu而言可能就只有一個線程。因此在這裏,張三和李四已經不是線程的概念了,而是協程的概念。

故事差很少就這樣,在這裏咱們關注到對於進程和線程的資源浪費,本質上是時間的浪費,而時間大部分就是花費在了空間上,同時合理的調度也是能省出一部分時間。

因此進程,線程等概念,我感受能夠當作是時間的概念,舉個例子:爽子一天掙208萬,那你就能夠用一爽比做208萬,由於從某個層面而言,一爽和208萬是等價的。固然你會觸類旁通:一東。

用了協程後,必定能你好,她也好嗎?

從上文中,在這種等待多的狀況下,你們自主的讓出cpu時間片會更高效。你可能會有一丟丟疑問,那幾個員工都想要同一個時間呢?讓出還有意義嗎?有的,由於不互相協調時間的話,那就最差的狀況就是:你們都被安排在本身忙的時間(io),但若是協調好時間的話,就能夠減小這種狀況。這種就是io密集型的場景,採用協做的方式能大大減小時間的浪費。

你們主動讓出的這種行爲就是協做,而不是像一開始那樣的都去搶佔時間片。這裏的讓出,其實就是掛起的意思,而掛起就意味着某個時間後仍是要恢復的,這裏和線程的狀態切換很像,但又不必定是真正的切換線程,下文會詳細講。

從上面的故事中能夠知道:從張三到李四的會議安排,是由你的祕書決定的,而主動讓出和何時開會,是由員工決定的,這裏的差異很大。在第二種方式下,你的祕書能夠省去不少找員工溝通的時間。

而若是張三李四的工做就是負責和你開會,不須要寫代碼,不須要畫稿子,不須要畫原型,那讓張三李四上線下線會議的行爲反而會影響張三和李四的工做效率,這種就是計算密集型的狀況,串行比並行高效。但其實有意思的是,有些語言的協程的調度機制作的不錯,在某些狀況下,不會頻繁的切換上下文。

協程的實現差別

某天合做夥伴孫總過來你這參觀,發現你管理員工的方法特別好,回去後立刻嘗試搞一套。也請了一個祕書,市場上不少這種管理專業的人才,這些人才的培養早有體系,找一個很簡單,因此孫總的公司很快就實現了經過祕書安排會議。這裏是在說不一樣操做系統線程的api設計大體相同。

但他遇到了個問題:他發現他的員工的協同能力比較差(也可能他發現他員工的協同能力很強,而後向你得意洋洋的炫耀)。員工的崗位,所掌握的技能,性格,素質水平等方面的不一樣,不通過培訓,很難一會兒要求全部人作到很好的協同,培訓的方式和效果也不同。因此有的公司可能最終協做的方案是A,有的公司協同方案是B。這裏實際上是在表達,協程是語言層面上的東西,每種語言的實現方式都不同,效果也不同。

小結

上文是從生活的角度去寫的,例子可能不那麼的恰當,但至少咱們知道進程爲了合理利用資源,作了努力,可是還有提高的空間。因此線程出現了,線程也作了努力,但仍是有提高空間。這時候協程出現了,也作了努力,至於提高空間,抱歉,我還處於熱戀期,眼裏都是協程的好。

協程也分有棧協程和無棧協程。常說調用棧調用棧,這裏的有棧其實就是由於一些協程間可能有調用關係,有個大佬講的很好:有棧協程和無棧協程

我是一名Android應用開發者,而kotlin中的協程是無棧協程,因此下文是我對於kotlin無棧協程的理解。下文的角度可能和上文不太同樣,生活中的例子落實到代碼實現,本就有差距,因此說大部分普通人一會兒難以理解程序開發,是由於沒有編程思惟嘛。

要想了解協程,就必須瞭解閉包的概念,固然咱們能夠從最熟悉的回調入手。

魔法:回調函數

image

在第一次接觸回調函數的時候,我相信不少人都會以爲這是個特別的東西。它真的很靈活,很神奇!咱們看看:

咱們常在一個函數中調用經過參數傳入的對象,進而使用這個對象的函數去作某事:

class LogPrinter {
  fun print() {
    ...
  }
}

fun printLog(logPrinter:LogPrinter) {
  logPrinter.print();
}
複製代碼

但你須要提早用類去描述這個行爲。可是咱們知道類所能描述的不只僅是一個行爲!

同時在事件驅動型的設計中,會有線程在不斷的等待事件和消費事件,就是併發中的「生產者消費者」模型,假設只有一個消費者,在一些程序中可能稱之爲主線程,那主線程便是生產者(存在本身給本身發事件的狀況),也是消費者。固然,大多生產者是其餘線程:

image

而事件的行爲和規則並非固定的,若是用類去提早描述一個行爲,是否是有點大材小用了?是否是要提早聲明一堆類?那有沒有一種東西能夠只針對行爲的東西,真正要用的時候再定義?

有啊,那不就是函數嗎?這裏就體現出了回調函數的靈活之處。

內部類我見過,在函數裏面聲明和實現函數是什麼鬼?就是閉包嘛!

java支持以參數形式傳遞函數嗎?一直都支持,只是java8比較明顯!

傳遞後的函數使用的this對象是誰?這裏講個故事你就明白了:

唐僧趕走了孫猴子,但大聖仍是懼怕師傅出事,便拔下了三根毛交給了八戒,告知八戒一出事就吹一根毛,俺老孫天然會出現。

同時爲了保證能持續消費事件,不能在主線程中有耗時的操做,而耗時操做常見的都是在計算數據,獲取數據等,主線程怎麼作到不等待又能在合適的時間拿到結果進行處理?

一個回調函數能解決上面的種種問題,神奇吧!爲何這麼神奇?

是的,逼逼了這麼多就是想看看你是否瞭解閉包,若是不瞭解,下面可能會看的雲裏霧裏,可是若是你都懂,恭喜你,下面的內容對你而言極其簡單!

不瞭解的話,先去查查,高階函數、閉包、java的匿名內部類吧!

回調魔法的特別之處

我這裏將從你們都頭疼的線程同步,從實際例子去講閉包的神奇特性在協程中的應用,來聊聊回調魔法的強大之處。

有這樣的一個場景:加載網頁資源並繪製。因爲加載網頁是個耗時的io操做,通常狀況下,咱們會將該耗時操做給另外一個線程去執行,以下圖所示:

image

主線程須要resource去繪製顯示,因此只能等待線程B執行完成,不然主線程將沒有內容能夠paint。雖然主線程沒有執行耗時操做,可是主線程會阻塞並等待線程B喚醒本身,這是很簡單的線程同步操做。

藝術源於生活,咱們的平常生活中也會有這種狀況,咱們的解決辦法就是:搞定了通知我。因此主線程應該在線程B通知本身加載資源完成後,纔去paint。

但咱們一般在這個期間會去作其餘的事情而不是等待,因此咱們但願主線程去繼續消費其餘事件,而不是在阻塞。

這個時候咱們可使用回調實現非阻塞的通知。

image

咱們傳入的函數回調,並無實現線程切換的功能,你暫且認爲這裏是線程B內部在invoke這個回調時作了線程切換,也就是將該回調包裝後放到隊列中,等待主線程消費,後面會詳細講線程切換。loadResource方法相似這個樣子:

fun loadResource(callBack:(Resource) ->Unit) {
  thread {
    val result = load() //耗時
    //包裝一下,發送給主線程,讓主線程執行callBack.invoke()
    sendToMainThread(){
      callBack.invoke(result)
    }
  }
}
複製代碼

這段代碼作到了:「作完通知我」,而且主線程並無被阻塞!也沒有由於執行這段代碼發生線程狀態的變化!這裏就能省下線程狀態切換帶來的開銷。

小結

仔細琢磨執行順序就能發現特別之處:

在沒有回調的代碼中,主線程的代碼執行順序是:

  1. 進入showHtmlPage方法
  2. 讓線程B執行loadResource方法
  3. 進入睡眠
  4. 從新喚醒,進行繪製
  5. 退出showHtmlPage方法
  6. 消費其餘事件

有回調的代碼中:主線程的執行順序是

  1. 進入showHtmlPage方法
  2. 讓線程B執行loadResource方法
  3. 退出showHtmlPage方法
  4. 消費其餘事件
  5. 線程B通知執行繪製

注意!在上面這種狀況中的showHtmlPage函數內部總體順序是不變的,順序必然是:

  1. 進入showHtmlPage方法
  2. 執行loadResource方法
  3. 執行繪製

但回調這個代碼塊相對於隊列中的其餘事件而言(事件其實也是一個個代碼塊),執行順序發生了變化。因此你要是以爲協程就是一個個待執行的代碼塊,對的!你摸到門道了!將要執行的代碼使用回調 」包裝起來「,是無棧協程實現執行狀態流轉的重要前提之一!而回調能在合適的時機執行!這是線程使得被包裝的代碼塊之間執行順序發送變化的最好體現!

魔法進階

咱們再看這樣的一個例子:

須要從不一樣的地址獲取資源,也是先用最簡單的同步方法去實現:(圖中的UIThread就是上文說的主線程,我畫圖一時懵逼,寫錯了)

image

問題也很明顯,主線程也被阻塞兩次,兩次線程同步形成了線程的狀態的切換,假設loadResourceA花10秒,loadResourceB花10秒,那從加載完到繪製,總共的時間,模糊估計花了20.10S,我又搬出了回調大法:

注意:(這裏的線程狀態切換耗費的時間爲0.1s,只是爲了好理解瞎編的,若是你很想知道線程切換的時間,能夠谷歌一下)

fun showHtmlPage() {
        val addrA = "A"
        val addrB = "B"

        loadResource(addrA) { resourceA ->
            loadResource(addrB) { resourceB ->
                paint(resourceFromA, resource)
            }
        }
    }
複製代碼

解決了阻塞問題,主線程有時間去作其餘事情,就算假設cpu等調度都不花時間,但代碼塊從加載完到繪製總共的時間也花了20S,縮短的都是在線程切換過程當中的時間,那也沒縮短多少秒。由於加載資源其實咱們仍是串行的。

這時候咱們繼續利用併發和回調去合理利用資源:

注意:(這裏的線程狀態切換耗費的時間爲0.1s,只是爲了好理解瞎編的,若是你很想知道線程切換的時間,能夠谷歌一下)

fun showHtmlPage() {
        val addrA = "A"
        val addrB = "B"
        var resourceFromA:Resource
        var resourceFromB:Resource

        loadResource(addrA) {resource ->
            if (resourceFromB!=null) {
                paint(resource,resourceFromB)
            }else {
                resourceFromA = resource
            }
        }
        loadResource(addrB) {resource ->
            if (resourceFromA!=null) {
                paint(resourceFromA,resource)
            }else {
                resourceFromB = resource
            }
        }
    }
複製代碼

我把兩個加載數據變成了並行,這時加載完到繪製總共的時間,模糊估計也花了10S,這裏縮短的就是串行的時機,由於兩個資源的加載是同時在進行的。

這時候你會在想:你兩個回調是否會有資源競爭問題?沒有!前面已經說了loadResource方法內部作了線程切換操做!!!!

但爲了更好的的理解爲何要作線程切換,咱們先假設存在資源競爭:也就是回調都是由不一樣的線程去invoke的,那你只能加鎖:(我就簡單粗暴的加鎖了,是否鎖對了,不要太糾結)

fun showHtmlPage() {
        val addrA = "A"
        val addrB = "B"
        var resourceFromA:Resource
        var resourceFromB:Resource

        loadResource(addrA) {resource ->
           synchronized(obj){
                if (resourceFromB!=null) {
                    paint(resource,resourceFromB)
                }else {
                    resourceFromA = resource
                }
           }             
        }
        loadResource(addrB) {resource ->
          synchronized(obj){
               if (resourceFromA!=null) {
                   paint(resourceFromA,resource)
               }else {
                   resourceFromB = resource
               }
           }
        }
    }
複製代碼

加了鎖後,加載完到繪製總共的時間,大膽一花了11.2s,多了這零點幾秒就是由於加了鎖,synchronized是互斥鎖,會阻塞其餘的線程。因此咱們爲了解決一個問題,引入了一個新問題。

我作線程切換的緣由:這個loadResource方法中內部就作了線程切換,這兩個回調必定是在主線程串行的,不存在多線程同時操做resourceFromA、resourceFromB的狀況。

小結

這點也頗有意思對吧!這也是利用協程解決資源競爭的一個思想:經過調整回調代碼塊的執行環境,將更改公共資源的代碼塊流轉到一個線程去執行,進而解決多線程資源競爭的問題。

但在loadResource方法內實現線程切換顯然是很差的設計,因此咱們須要設計一個調度器,去指定回調在哪一個線程執行,是否是在必定程度上能夠解決多線程併發的資源競爭問題?同時上文的代碼中,加載資源會初始化兩個線程,那加載100個資源是否是須要100個線程?線程資源的建立和釋放又是一個消耗資源的操做,這個又怎麼解決?

解決資源競爭

若是開發者合理的利用調度器:指定操做公共資源的回調在同一個線程執行,這樣就不會有資源競爭問題。那可讓程序員在寫的時候,就指定該代碼段(回調)會在哪一個線程執行:

loadResource(UIThread,addrB) { resourceB ->
                paint(resourceFromA, resource)
    }
複製代碼

因此這個調度器若是針對線程環境實現,會更加靈活。那咱們能夠給上文的主線程單獨設計一個符合下圖模型的調度器,:

image

解決頻繁建立和銷燬線程的性能消耗

我相信不少程序員都知道怎麼解決頻繁建立和釋放線程的問題,是的沒錯,就是線程池。

我在上文有提到:

若是張三李四的工做就是負責和你開會,不須要寫代碼,不須要畫稿子,不須要畫原型,那讓張三李四上線下線會議的行爲反而會影響張三和李四的工做效率

是否能夠根據io密集型或運算密集型,選擇不一樣的調度器?由於只有開發人員知道是哪一種類型。

因此咱們能夠:

針對運算型的調度器的線程池能夠根據cpu核心數去建立線程數量。

針對io密集型的調度器的線程池能夠建立好多個線程。

那這就有三個基本的調度器了!但在上文提到loadResource方法中內部就作了線程切換,假設有不少相似於loadResource的方法,他們是否是也要本身作線程切換?這不符合單一職責對吧?

創造新魔法,讓回調具有線程切換能力

注意!如下是我爲了好理解思想,用java寫出來的代碼 ,kotlin具體實現協程的代碼不徹底是這樣的!

(爲何是java?由於我用kotlin寫這個例子感受怪怪的)

定義通用回調接口

在java中的回調是匿名內部類,因此咱們聲明個接口,用來給使用方實例化,做爲通用回調,這個回調供用戶包裝本身的耗時同步代碼,因此須要返回值。

public interface FakerCallBack {
    public Object invokeSuspend();
}
複製代碼

續體傳遞風格

異步拿結果,必然是經過回調,這裏要提一下什麼叫續體傳遞風格,其實就是經過回調將結果傳遞給調用方的意思,舉個簡單例子:

通常寫法:

public String getWelcomeSpeech() {
  return "welcome";
}

public void sayWelcome() {
  System.out.println(getWelcomeSpeech());
}
複製代碼

續體傳遞風格:

interface FakerCompletionResult {
    public void resumeWith(Object value);
}


public void getWelcomeSpeech(FakerCompletionResult completion) {
  completion.resumeWith("welcome");
}

public void sayWelcome() {
  getWelcomeSpeech(value -> {
       System.out.println(value)    
  });
}
複製代碼

因此要定一個能夠傳遞結果的回調

public interface FakerContinuation {
    public void resumeWith(Object obj);
}
複製代碼

Runnable -》 Job

咱們要切換協程,線程認哪一個回調?是的,Runnable,他也是個接口,這裏爲了區分開,我定義一個Job(不算屢次一舉哈,由於這裏就是要和線程的區分開,寫一個好理解):

public interface FakerJob {
    public void start();
}
複製代碼

回調攔截

咱們上面定義的回調接口不具有指定線程這些功能,因此咱們須要定義一個外層回調去包裝用戶傳入的代碼塊,賦予數線程切換的能力,就像AOP。

public interface FakerInterceptor {
    public void dispatch(FakerJob callBack);
}
複製代碼

調度器

調度器就是攔截器的實現,爲何?由於要先攔截了,而後才能在指定線程中執行回調嘛!

我寫了兩個調度器,

  1. 固定20個線程的線程池實現的IO密集型調度器。

  2. 使用Handler實現切換到主線程執行的調度器。

你要不是個Android開發者,可能不知道什麼是Handler。那你能夠寫個單線程的線程池的調度器,而後實現本身的消息隊列,至於如何實現延遲事件執行,達到模擬線程的sleep功能。只須要記住一個思想:延遲不是阻塞,只是掛起這個代碼塊,讓該線程去消費其餘代碼塊,到時間再回來執行。固然若是線程原本就空閒,那消息隊列的消費者也是會有等待的狀況,實在不會寫能夠參考android的handler實現。

public class IODispatchers implements FakerInterceptor {
    private ExecutorService executor = Executors.newFixedThreadPool(20);

    @Override
    public void dispatch(FakerJob callBack) {
        executor.execute(callBack::start);
    }
}

public class MainDispatchers implements FakerInterceptor {
    private Handler executor = new Handler(Looper.getMainLooper());

    @Override
    public void dispatch(FakerJob callBack) {
        executor.post(callBack::start);
    }
}
複製代碼

寫個調度器的聲明類:

public class FakerDispatchers {
    public static FakerInterceptor IO = new IODispatchers();
    public static FakerInterceptor Main = new MainDispatchers();
}
複製代碼

回調構造

咱們還須要定義一個回調構造器,統一對外api,讓用戶方便使用這個強大的回調,有些不須要結果也不執行耗時操做的,callBack能夠直接傳入null:

public class FakerCompletionBuilder {

    public static void launch(FakerInterceptor dispatchers,FakerCallBack callBack, FakerContinuation fakerContinuation) {
        dispatchers.dispatch(new FakerJob() {
            @Override
            public void start() {
                if (callBack!=null) {
                    fakerContinuation.resumeWith(
                            callBack.invokeSuspend()
                    );
                }else {
                    fakerContinuation.resumeWith(null);
                }
            }
        });
    }
}
複製代碼

指定線程執行

是騾子是馬,拉出來溜溜!我直接在android真機跑,Activity的代碼都是kotlin了懶得改代碼了,差很少的,你應該看得懂:

(2021.6.21 修改:)

fun launchTest() {
         FakerCompletionBuilder.launch(FakerDispatchers.IO, {
            Log.d(">>> MainActivity", "launchTest ${Thread.currentThread().name}")
            val r = loadResult("addrA")
        }) {
            FakerCompletionBuilder.launch(
                FakerDispatchers.Main, null
            ) {
                Log.d(">>> MainActivity", "launchTest r = $r ${Thread.currentThread().name}")
            }
        }
    }

    private fun loadResult(addr: String): String? {
        try {
            if (addr == addrA) {
                Thread.sleep(1000)
            } else {
                Thread.sleep(3000)
            }

        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
        return "load end $addr"
    }
複製代碼

注意,這裏的 Thread.sleep是爲了模擬耗時,因此此次不算是線程狀態變化哈。運行結果:

>>> MainActivity: launchTest  pool-1-thread-1
>>> MainActivity: launchTest result = load end addrA  main
複製代碼

很順利就切換了線程!

資源競爭

因爲我這是android程序,若是在主線程阻塞等待,結果會有偏差,因此我再加一個單線程的調度器:

public class SingleDispatchers implements FakerInterceptor {
    private ExecutorService executor = Executors.newSingleThreadExecutor();

    @Override
    public void dispatch(FakerJob callBack) {
        executor.execute(callBack::start);
    }
}


public class FakerDispatchers {
    public static FakerInterceptor IO = new IODispatchers();
    public static FakerInterceptor Main = new MainDispatchers();
    public static FakerInterceptor Single = new SingleDispatchers();
}
複製代碼

使用:

var i = 0
    var j = 0

    fun testResourceCompetition() {
        (1..10000).forEach {
            load()
        }

        Thread.sleep(10000)
        println("i $i")
        println("j $j")
    }


    fun load() {
        FakerCompletionBuilder.launch(FakerDispatchers.IO, null) {
            j++
            FakerCompletionBuilder.launch(FakerDispatchers.Single,null) {
                 i++
            }
        }
    }
複製代碼

結果:

I/System.out: i 10000
I/System.out: j 9869
複製代碼

j的值每次都是不同,而i的值一直是10000。不須要加鎖就解決了資源競爭,有意思吧!

加載多個異步資源

若是利用上文實現的FakerCompletionBuilder,進行並行加載資源:

fun loadDouble() {
        var resultA: String? = null
        var resultB: String? = null
        FakerCompletionBuilder.launch(FakerDispatchers.IO, { loadResource(addrA) }) {result ->
            FakerCompletionBuilder.launch(FakerDispatchers.Main, null) {
                resultA = result.toString()
                if (resultB != null) {
                    Log.d(">>> MainActivity", "result = $resultA $resultB")
                }
            }
        }

        FakerCompletionBuilder.launch(FakerDispatchers.IO, { loadResource(addrB) }) {result->
            FakerCompletionBuilder.launch(FakerDispatchers.Main, null) {
                resultB = result.toString()
                if (resultA != null) {
                    Log.d(">>> MainActivity", "result = $resultA $resultB")
                }
            }
        }
    }
複製代碼

小結

實現一個簡單的線程切換的回調不難,在最後一個例子中也給出了同步兩個異步線程結果的方式,真正的核心代碼執行的順序是這樣:

//這兩個咱們但願並行
loadResource(addrA) 
loadResource(addrB) 
//但最後必定是:
Log.d(">>> MainActivity", "result = $resultA $resultB") 
複製代碼

我爲了實現這種效果,使用回調將三條語句拆開,並分別包裝,這裏能夠說:我給這幾條語句放在了不一樣的協程裏。

而後經過不一樣的線程去執行了loadResource所在的協程,最後使用使用控制流,讓結果輸出的協程在最後執行。

回顧上文可知:能夠經過線程影響這些回調代碼塊的順序。但從最後這個例子中,還能夠看到:想要從順序不受控的協程執行後,獲得指望順序的執行結果,還可使用控制流去處理!沒有阻塞,沒有鎖!有意思吧!

從上面的代碼看到,條件判斷語句會有一些重複代碼:

resultB = result.toString()
 if (resultA != null) {
     Log.d(">>> MainActivity", "result = $resultA $resultB")
 }

 resultA = result.toString()
 if (resultB != null) {
     Log.d(">>> MainActivity", "result = $resultA $resultB")
 }
複製代碼

是否是以爲,我應該抽出一個函數?不!千萬不要!由於我在回調函數那一小結說了:我但願的是在某個函數中聲明咱們想要的函數,而不是在類中又去聲明另外一個函數。簡單的說:不想爲了改變這個函數的內部行爲,污染了這個函數所屬的類!因此下面我來解決這重複代碼。

當回調碰見switch,會擦出什麼火花?

當有不少條語句控制條件,咱們會用switch語句,控制條件能夠給個狀態值。是的,就是狀態機:

public void test(state : Int) {
  switch (state) {
    case 0: {
      loadResource(addrA);	
      break;
    }
    case 1: {
      loadResource(addrB) 
      break;
    }
    case 2: {
      Log.d(">>> MainActivity", "result = $resultA $resultB")
      break;  
    }
  }
}
複製代碼

經過傳入的state的值,能夠控制這幾句代碼的執行順序。

但誰調用這個方法?誰存儲轉態值?

  • 方案1 :外部寫一個循環,改變狀態的值,調用這個方法

但在不聲明新函數?不定義新成員變量的狀況下,怎麼實現?

  • 是的,閉包!回調!匿名內部類!nice!咱們能夠這樣寫:
public void loadDouble() {
  //實例化一個匿名內部類,也就是咱們所說的回調,包裝整個loadDouble方法內部的代碼
  FakerContinuation continuation= new FakerContinuation() {
    	//初始化狀態
      int state = 0;
    	//接收兩個結果
      String resultA = null;
      String resultB = null;

      @Override
      public void resumeWith(Object obj) {
          switch (state) {
              case 0: {
                	//將加載A的代碼包裝起來,丟給子線程執行,這個launch方法前面沒看到,下面會講,注意這裏傳了個this
                  FakerCompletionBuilder.launch(FakerDispatchers.Main, FakerDispatchers.IO, this, new FakerCallBack() {
                      @Override
                      public Object invokeSuspend() {
                        	//加載資源A
                          return loadResource("A");
                      }
                  }, new FakerContinuation() {
                      @Override
                      public void resumeWith(Object obj) {
                        	//加載資源A完成後,存儲值
                          resultA = obj.toString();
                      }
                  });
                  FakerCompletionBuilder.launch(FakerDispatchers.Main, FakerDispatchers.IO, this, new FakerCallBack() {
                      @Override
                      public Object invokeSuspend() {
                         	//加載資源B
                          return loadResource("B");
                      }
                  }, new FakerContinuation() {
                      @Override
                      public void resumeWith(Object obj) {
                          //加載資源B完成後,存儲值
                          resultB = obj.toString();
                      }
                  });
                	//將狀態切換爲1
                  state = 1;
                  return; //退出最外層的resumeWith方法
              }
              case 1: {
                	//下一次調用resumeWith方法,會進入到該狀態, 進行判斷,下一次誰調用,下面會講
                  if (resultA == null || resultB == null) {
                      return;
                  }
              }
          }
        	//輸出結果
          Log.d(">>> ", resultA + " " + resultB);
      }
  };
  //手動執行父閉包
  continuation.resumeWith(null);
 }
複製代碼

利用閉包的特性,聲明瞭閉包內部的成員變量,而後又啓動了兩個協程進行資源的加載,可是在狀態0的時候已經return了,爲何會從新進入到switch?

那是由於我新增了一個啓動協程的方法:

//傳了父閉包的調度器,和父閉包
public static void launch(FakerInterceptor dispatchersParen, FakerInterceptor dispatchers, FakerContinuation continuationParen, FakerCallBack callBack, FakerContinuation continuation) {
        dispatchers.dispatch(new FakerJob() {
            @Override
            public void start() {
                Object obj = callBack.invokeSuspend();
                continuation.resumeWith(obj);

              	//注意這裏,使用父閉包的調度器執行從新執行了父閉包的resumeWith,並將結果傳過去了,因此剛剛的switch能夠執行
                dispatchersParen.dispatch(new FakerJob() {
                    @Override
                    public void start() {
                        continuationParen.resumeWith(obj);
                    }
                });
            }
        });
    }
複製代碼

不是說不加新方法嗎?哈哈,我這裏加的是一個通用的協程啓動方法,要想實現上文說的效果,最好的方式就是加這個方法啦!並且該方法是FakerCompletionBuilder中定義的,並無污染本來的類和函數。

爲何作了個父閉包調度器的切換?必須的呀!由於咱們指望的就是下面這樣的執行效果:

suspend fun showHtmlPage () {
  val resultA = async{loadResource(addrA)} //異步線程執行
  val resultB = async{loadResource(addrB)} //異步線程執行
  log(resultA,resultB) //主線程執行
}
複製代碼

同樣沒有加鎖!判斷語句再也不是重複代碼!看吧!switch和回調在一塊兒,繞是挺繞的,可是頗有意思對吧!這段其實已經很接近協程了,思想其實已經體現出來了,建議再細細品味一下!!!

小結

switch將須要異步執行和同步執行的語句,分別劃分在不一樣的閉包中,每執行完一個閉包就改變lable的值(改變執行狀態),而後經過子閉包從新調用父閉包的回調方法,父閉包的resumeWith方法從新進入後,因爲狀態改變了,因此執行了不一樣與上一次的代碼塊。就實現了在父閉包中,各個不一樣代碼塊執行狀態的流轉,進而實現表現上的同步獲得結果。

認真想一想,其實就是將一個完整的代碼塊,根據開發者的聲明(例如:launch{}),拆成不一樣的代碼塊,使用閉包包裝起來,而後根據各個代碼塊的狀況調整狀態,改變了全部代碼塊的執行順序。注意!這裏強調了開發者的聲明,很明顯,分塊實際上是由開發者決定的!也就是程序決定的!

這裏印證了上文的:將要執行的代碼使用回調 」包裝起來「,在合適的時機執行!這是線程回調使得代碼執行順序流轉的最好體現!也是無棧協程實現執行狀態流轉的重要前提之一!

這裏父閉包,啓動子閉包,子閉包又從新調用父閉包,其實能夠看作一個特殊的循環!!可是是遞歸嗎?這個問題留給你!

鬱悶

可是有兩個地方很鬱悶:

1.切換到父閉包的調度器去回調resumeWith方法,而個人主線程調度器是將閉包丟到了隊列裏面,若是都是在主線程中,又得等一會才能執行,這個等一下是否必要?

2.咱們明明已經將結果經過父閉包的resumeWith方法返回了,爲何switch中還要在子閉包中的resumeWith方法賦值?

//傳了父閉包的調度器,和父閉包
public static void launch(FakerInterceptor dispatchersParen, FakerInterceptor dispatchers, FakerContinuation continuationParen, FakerCallBack callBack, FakerContinuation continuation) {
     ....

              	//注意這裏,使用父閉包的調度器執行從新執行了父閉包的resumeWith,並將結果傳過去了,因此剛剛的switch能夠執行
                dispatchersParen.dispatch(new FakerJob() {
                    @Override
                    public void start() {
                      //這裏!!!
                        continuationParen.resumeWith(obj);
                    }
                });
            }
        });
    }
複製代碼
...
  }, new FakerContinuation() {
                      @Override
                      public void resumeWith(Object obj) {
                        	//加載資源A完成後,存儲值
                          resultA = obj.toString();
                      }
                  });
  ...
複製代碼

那是由於我不知是誰傳進來的結果,沒法確認是結果A仍是結果B!簡單地說:我不知道A和B的回調順序!

這裏不是Kotlin協程的真正實現,是我本身寫的例子,爲何會有這麼奇怪的實現?由於不想這麼早丟出await,客官不要急嘛!等到下文講到await後,這個問題就迎刃而解了!

是否值得掛起

切換到了父閉包的調度器去回調resumeWith方法,由於我這裏的主線程調度器是將閉包丟到了隊列裏面,若是都是在主線程中,又得等一會才能執行,這個等一下是否必要?

是的,我感受不必!因此kotlin中有標誌聲明是可能掛起!而不是必定掛起!如:函數開頭中的suspend的關鍵字、閉包執行的返回值。

咦!不知不覺就告訴了你:掛起,簡單的理解就是暫時不執行這個閉包!

因此咱們能夠用最簡單的方式模擬一下,前提條件是同一個線程,請牢記這個!

因爲在kotlin中,launch的返回值是個job,因此我以爲使用async更貼切一點,注意!kotlin的實現不是這樣的,可是爲了讓你更加簡單的明白爲何不須要掛起,更簡單的說明掛起的思想,就手寫了這個例子,請務必知悉:

//定義一個標誌位
final int SUSPEDN = 1//增長一個返回值
public static Object async(FakerInterceptor dispatchersParen, FakerInterceptor dispatchers, FakerContinuation continuationParen, FakerCallBack callBack, FakerContinuation continuation) {
    	//注意這裏,若是調度器不等於父協程的調度器,就掛起
        if (dispatchers != dispatchersParen) {
            dispatchers.dispatch(new FakerJob() {
                @Override
                public void start() {
                    Object obj = callBack.invokeSuspend();
                    continuation.resumeWith(obj);

                    dispatchersParen.dispatch(new FakerJob() {
                        @Override
                        public void start() {
                            continuationParen.resumeWith(obj);
                        }
                    });
                }
            });
          	//返回這個標誌位
            return SUSPEDN;
        } else { //不然不必掛起
            Object obj = callBack.invokeSuspend();
            continuation.resumeWith(obj); //子閉包也傳遞結果
            return obj; //直接執行,直接返回結果
        }
    }
複製代碼

就是返回一個標誌位告訴switch是否須要切換狀態而已,使用看看就知道了:

public void loadDouble() {
        FakerContinuation continuation= new FakerContinuation() {
            int state = 0;
            String resultA = null;
            String resultB = null;
            Object resultLaunchB ;

            @Override
            public void resumeWith(Object obj) {
                switch (state) {
                    case 0: {
                        Object resultLaunchA = FakerCompletionBuilder.async(FakerDispatchers.Main, FakerDispatchers.Main, this, new FakerCallBack() {
                            @Override
                            public Object invokeSuspend() {
                                return loadResource("A");
                            }
                        }, new FakerContinuation() {
                            @Override
                            public void resumeWith(Object obj) {
                                resultA = obj.toString();
                            }
                        });
                        resultLaunchB = FakerCompletionBuilder.async(FakerDispatchers.Main, FakerDispatchers.IO, this, new FakerCallBack() {
                            @Override
                            public Object invokeSuspend() {
                                return loadResource("B");
                            }
                        }, new FakerContinuation() {
                            @Override
                            public void resumeWith(Object obj) {
                                resultB = obj.toString();
                            }
                        });            
                        state = 1;
                      	//注意這裏,決定了要不要跳閉包等待下一次狀態切換,仍是繼續執行代碼
                        if (resultLaunchA == SUSPEDN) {
                            return;
                        }else {
                            resultA = resultLaunchA.toString();
                        }
                        if (resultLaunchB == SUSPEDN) {
                            return;
                        }else {
                            resultB = resultLaunchB.toString();
                        }
                    }
                    case 1: {
                        if (resultA == null || resultB == null) {
                            return;
                        }
                        break;
                    }
                }
                Log.d(">>> ", resultA + " " + resultB);
            }
        };
        continuation.resumeWith(null);
    }
複製代碼

要不要繼續跳出switch,是由async的返回值決定的,因此要不要掛起,其實就是指:

  • 是應該將兩個代碼分紅兩個代碼塊,而後有順序的執行;
  • 或是不分開兩部分代碼,直接執行下去。

這下咱們知道了,是否掛起,是不必定的,要看具體條件,在kotlin中就是經過返回值聲明的哦!

實現delay

但有個狀況是必然會掛起的!那就是delay!delay的意思很明確,就是延遲一下再執行,體現掛起這個行爲的最佳例子!

咱們知道Thread.sleep是會阻塞的,delay爲何不會阻塞?而是掛起?若是你已經理解了上文的內容,其實你心中已經有答案了!不理解也不要緊,再往下看看說不定就清晰了!

咱們要實現這樣的效果:

log(1)
delay(1000) //延時一秒後再往下執行,可是線程不阻塞
log(2)
複製代碼

注意! 我這裏就使用主線程調度器的實現,由於簡單,爲何簡單?android開發者看到後,必定會直呼內行!

先改改主線程的調度器:

public class MainDispatchers implements FakerInterceptor {
    private Handler executor = new Handler(Looper.getMainLooper());

    @Override
    public void dispatch(FakerJob callBack) {
        executor.post(callBack::start);
    }

  	//是的,就是postDelayed,驚不驚喜意不意外?,因此說生產者消費者模型真的很厲害!
    @Override
    public void dispatch(Long time,FakerJob callBack) {
        executor.postDelayed(callBack::start,time);
    }
}

複製代碼

再建立一個delay的構造器:

public static Object delay(Long time, FakerInterceptor dispatchersParen, FakerContinuation continuationParen) {
        dispatchersParen.dispatch(time, new FakerJob() {
            @Override
            public void start() {
              	//延遲結束
                continuationParen.resumeWith(SUSPEDN_FINISH);
            }
        });
      	//延遲
        return SUSPEDN;
    }
複製代碼

使用:

public void test() {
        FakerContinuation continuation = new FakerContinuation() {
            int state = 0;
            Object resultLaunchB;

            @Override
            public void resumeWith(Object obj) {
                switch (state) {
                    case 0: {
                        Log.d(">>> ", "1");
                        state = 1;
                      	//注意這個this,就是父閉包的引用,因此延時任務是從新回調父閉包的resumeWith方法
                        Object result = FakerCompletionBuilder.delay(1000L, FakerDispatchers.Main, this);
                        if (result == SUSPEND) {
                            return;
                        }
                    }
                    case 1: {
                        Log.d(">>> ", "2");
                    }
                }
            }
        };
        continuation.resumeWith(null);
    }
複製代碼

簡單吧,就是加入隊列,而後排隊去了!到了時間後纔回到父閉包的resumeWith,從而進入到狀態2。這裏就像本文一開始的故事中寫的同樣:

log2主動讓出了1秒!而後主線程去消費隊列中的執行其餘事件!

實現await

來看看上文提到的另外一個問題:

咱們明明已經將結果經過父閉包的resumeWith方法返回了,也就是咱們說的續體傳遞風格,爲何switch中還要在子閉包中的resumeWith方法賦值?

由於在不給返回結果添加標誌位的狀況下,很差控制加載資源A和B的返回結果!那我換個思路,給個對象去存儲異步的結果,當結果到了時候,通知父閉包。什麼意思呢?

java程序員大多知道Future,由於runnable沒有結果返回,正如咱們上文的Job同樣:

public interface FakerJob {
    public void start();
}
複製代碼

因此java提供了個Future來獲取runnable執行完的結果,但Future是阻塞的,而協咱們要實現的是非阻塞的Future:

public interface LightFuture {
    public Object await(FakerContinuation continuation);
}
複製代碼

是否是有deferred的味道了?看看具體實現吧,都在註釋裏面了:

public class LightFutureImpl implements LightFuture, FakerContinuation {

  	//狀態,異步協程是否執行完畢
    public boolean isCompleted = false;
  	//存儲異步協程執行的結果
    public Object result;
  	//父閉包的引用
    public FakerContinuation continuation;

  	//這個方法很熟悉了,傳入父閉包,若是結果已經有了,直接返回,若是沒有,告知父閉包這裏應該掛起!
    public Object await(FakerContinuation continuation) {
        this.continuation = continuation;
        if (isCompleted) {
            return result;
        }
        return FakerContinuation.SUSPEDN;
    }

  	//異步協程通知該Future結果已經獲取了
    @Override
    public void resumeWith(Object obj) {
        isCompleted = true;
        result = obj;
      	//父協程沒有,將結果存下來,可是不回調
        if (continuation != null) {
          	//通知父協程結果已經拿到,能夠進入到下一個狀態了
            continuation.resumeWith(obj);
        }
    }
}
複製代碼

定義咱們的async構造器:

public static LightFuture async(FakerInterceptor dispatchersParen, FakerInterceptor dispatchers, FakerCallBack callBack) {
  			//實例化一個LightFutureImpl
        LightFutureImpl lightFuture = new LightFutureImpl();
        dispatchers.dispatch(new FakerJob() {
            @Override
            public void start() {
                Object result = callBack.invokeSuspend();
              	//注意這是切換回了父協程的調度器
                dispatchersParen.dispatch(new FakerJob() {
                    @Override
                    public void start() {
                      	//注意看這裏,在異步協程執行完成後,將結果存到LightFutureImpl中
                        lightFuture.resumeWith(result);
                    }
                });
            }
        });
        return lightFuture;
    }
複製代碼

使用:

public void test() {
        FakerContinuation continuation = new FakerContinuation() {
            int state = 0;
          	//用來獲取結果B的Future
            LightFuture futureB;
            Object resultB;

            @Override
            public void resumeWith(Object obj) {
              	//注意,這裏定義一個做用域lable17
                lable17:
                {
                  	//存儲其餘協程傳遞的結果
                    resultB = obj;
                    switch (state) {
                        case 0: {
                          	//啓動協程,加載資源A
                            LightFuture futureA = FakerCompletionBuilder.async(FakerDispatchers.Single, FakerDispatchers.IO, new FakerCallBack() {
                                @Override
                                public Object invokeSuspend() {
                                    return loadResource("A");
                                }
                            });
                          //啓動協程,加載資源B
                            futureB = FakerCompletionBuilder.async(FakerDispatchers.Single, FakerDispatchers.IO, new FakerCallBack() {
                                @Override
                                public Object invokeSuspend() {
                                    return loadResource("B");
                                }
                            });
                          	//狀態改成1
                            state = 1;
                          	//嘗試去取結果,注意這裏傳入了父閉包的引用
                            Object result = futureA.await(this);
                          	//取不到結果,直接掛起,退出這個父閉包,等待下一個狀態的到來
                            if (result == FakerContinuation.SUSPEDN) {
                                return;
                            }
                        }
                        //協程A執行完成,進入了這個狀態,但咱們啥也不幹直接跳出switch,爲何必定是協程A,稍後講
                        case 1: {
                            break;
                        }
                        case 2: {
                          	//直接跳出lable17,注意是lable17!
                            break lable17;
                        }
                    }
                  	//從狀態1到這裏,輸出協程A執行的結果
                    System.out.println(">>>"+obj.toString());
                  	//狀態切到2
                    state = 2;
                    //嘗試取出結果,注意這裏傳入了父閉包的引用
                    resultB = futureB.await(this);
                    //嘗試取不到,就掛起,等待協程B回調該父閉包,進入狀態2
                    if (resultB == FakerContinuation.SUSPEDN) {
                        return;
                    }

                }// lable17 end
              	//這句必定是從狀態2過來的,因此直接輸出B結果
                System.out.println(">>>"+resultB.toString());
            }
        };
     
        continuation.resumeWith(null);
    }

    private String loadResource(String addr) {
        try {
            if (addr.equals("A")) {
                Thread.sleep(1000);
            } else {
                Thread.sleep(2000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return "load end " + addr;
    }
複製代碼

很神奇是嗎?爲何必定是協程A進入狀態1?

由於咱們在狀態1前都沒有調用過:

//嘗試取出結果,注意這裏傳入了父閉包的引用
resultB = futureB.await(this);
複製代碼

因此協程B就算執行完成了,也不會回調父閉包的resumeWith方法,由於咱們有個判空還記得嗎?:

public class LightFutureImpl implements LightFuture, FakerContinuation {

    public boolean isCompleted = false;
    public Object result;
    public FakerContinuation continuation;

    public Object await(FakerContinuation continuation) {
        this.continuation = continuation;
        if (isCompleted) {
            return result;
        }
        return FakerContinuation.SUSPEDN;
    }

    @Override
    public void resumeWith(Object obj) {
        isCompleted = true;
        result = obj;
      	//這裏
        if (continuation != null) {
            continuation.resumeWith(obj);
        }
    }
}
複製代碼

因此狀態1必定是協程A進入的!

其實就將結果暫時存到了別的地方,你取的時候多是直接取到,也可能被掛起,等到父閉包傳遞進來。實現告終果的同步,可是又不阻塞線程。一個簡易版的async就實現啦!

注意!kotlin的await實現很複雜,網上有不少種關於阻塞仍是不阻塞的討論

  • 有些人說await寫死了while阻塞:(我不認同這個觀點)
@Override
    public void resumeWith(@NotNull Object result) {
        synchronized (this){
            this.result = result;
            notifyAll(); // 協程已經結束,通知下面的 wait() 方法中止阻塞
        }
    }

    public void await() throws Throwable {
        synchronized (this){
            while (true){
                Object result = this.result;
                if(result == null) wait(); // 調用了 Object.wait(),阻塞當前線程,在 notify 或者 notifyAll 調用時返回
                else if(result instanceof Throwable){
                    throw (Throwable) result;
                } else return;
            }
        }
    }
複製代碼
  • 有些人說是編譯後將同步代碼變成了回調(很神奇的操做,可是我也不認同)

小結

其實kotlin的LightFuture也就是Deferred,真正實現是CompletableDeferredImpl,是繼承自Job的,由於Job有狀態,而LightFuture也有個完成(isCompleted)狀態。但怕信息太複雜,你一下看懵了,因此我沒搞那麼複雜去繼承FakerJob。

kotlin關於await的源碼超級複雜,看着看着會懵逼,並且因爲黑魔法的存在,直接看kotlin層的源碼我感受會看傻人。這裏你可能有兩個疑問:

  1. 我怎麼證實本文中的await實現是對的?
  2. 若是每次想要用java實現協程,那不是都得寫switch?那代碼不是又長又臭?

黑魔法

回答問題1 :

我怎麼證實本文中的await實現是對的?我不保證我必定正確,我是閱讀了kotlin代碼decompile後的java代碼,認爲實現方式多是這樣,下文會給出decompile後的java代碼和kotlin代碼的對比。

回答問題2:

沒辦法,java自己就不支持協程,但由於協程是語言層面的東西,因此我才能徹底經過java代碼實現協程。下文會給出decompile後的java代碼,你就知道爲何kotlin寫個協程這麼簡單了。

黑魔法是指編譯器對咱們寫下的kotlin代碼作了處理,例如咱們寫下的是這樣的代碼:

suspend fun showHtmlPage() = runBlocking {
        val resultA = async { loadResource("addrA") }
        val resultB = async { loadResource("addrB") }
        Log.d(">>>", resultA.await().toString()+resultB.await().toString())
    }
複製代碼

decompile後的java代碼是這樣的:

若是你理解了上文的全部思想,相信看懂這部分代碼必定不難,給點耐心:

public final Object showHtmlPage(@NotNull Continuation $completion) {
      return BuildersKt.runBlocking$default((CoroutineContext)null, (Function2)(new Function2((Continuation)null) {
         // $FF: synthetic field
         private Object L$0;
         Object L$1;
         Object L$2;
         int label;

         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            Object var10000;
            String var5;
            StringBuilder var6;
            Object var7;
            label17: {
               Object var8 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
               Deferred resultB;
               switch(this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  CoroutineScope $this$runBlocking = (CoroutineScope)this.L$0;
                  //注意這
                  Deferred resultA = BuildersKt.async$default($this$runBlocking, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
                     int label;

                     @Nullable
                     public final Object invokeSuspend(@NotNull Object var1) {
                        Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                        switch(this.label) {
                        case 0:
                           ResultKt.throwOnFailure(var1);
                           return MainActivity.this.loadResource("addrA");
                        default:
                           throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                        }
                     }

                     @NotNull
                     public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
                        Intrinsics.checkNotNullParameter(completion, "completion");
                        Function2 var3 = new <anonymous constructor>(completion);
                        return var3;
                     }

                     public final Object invoke(Object var1, Object var2) {
                        return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
                     }
                  }), 3, (Object)null);
                  //注意這裏
                  resultB = BuildersKt.async$default($this$runBlocking, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
                     int label;

                     @Nullable
                     public final Object invokeSuspend(@NotNull Object var1) {
                        Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                        switch(this.label) {
                        case 0:
                           ResultKt.throwOnFailure(var1);
                           return MainActivity.this.loadResource("addrB");
                        default:
                           throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                        }
                     }

                     @NotNull
                     public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
                        Intrinsics.checkNotNullParameter(completion, "completion");
                        Function2 var3 = new <anonymous constructor>(completion);
                        return var3;
                     }

                     public final Object invoke(Object var1, Object var2) {
                        return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
                     }
                  }), 3, (Object)null);
                  var6 = new StringBuilder();
                  var5 = ">>>";
                  this.L$0 = resultB;
                  this.L$1 = var5;
                  this.L$2 = var6;
                  this.label = 1;
                  //注意這裏!
                  var10000 = resultA.await(this);
                  if (var10000 == var8) {
                     return var8;
                  }
                  break;
               case 1:
                  var6 = (StringBuilder)this.L$2;
                  var5 = (String)this.L$1;
                  resultB = (Deferred)this.L$0;
                  ResultKt.throwOnFailure($result);
                  var10000 = $result;
                  break;
               case 2:
                  var6 = (StringBuilder)this.L$1;
                  var5 = (String)this.L$0;
                  ResultKt.throwOnFailure($result);
                  var10000 = $result;
                  //注意這裏
                  break label17;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
               }

               var7 = var10000;
               var6 = var6.append(String.valueOf(var7));
               var5 = var5;
               this.L$0 = var5;
               this.L$1 = var6;
               this.L$2 = null;
              //注意這裏
               this.label = 2;
               var10000 = resultB.await(this);
               if (var10000 == var8) {
                  return var8;
               }
            }

            var7 = var10000;
           //注意這裏
            return Boxing.boxInt(Log.d(var5, var6.append(String.valueOf(var7)).toString()));
         }

         @NotNull
         public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
            Intrinsics.checkNotNullParameter(completion, "completion");
            Function2 var3 = new <anonymous constructor>(completion);
            var3.L$0 = value;
            return var3;
         }

         public final Object invoke(Object var1, Object var2) {
            return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
         }
      }), 1, (Object)null);
   }

   private final String loadResource(String addr) {
      try {
         if (Intrinsics.areEqual(addr, "A")) {
            Thread.sleep(2000L);
         } else {
            Thread.sleep(1000L);
         }
      } catch (InterruptedException var3) {
         var3.printStackTrace();
      }

      return "load end " + addr;
   }
複製代碼

能夠看到不管協程裏面作了啥,都會生成switch語句,我估計是爲了統一輩子成吧。

和我上文實現的是否是差很少?因此到底async到底如何實現的,我就是經過這裏得出的結論,而且寫出了測試代碼,得出的結果也比較符合kotlin的async。我工做不到一年,水平有限,因此真正的實現方式仍是得你們夥分析論證。

若是你不糾結實現方式,而是思想,我很高興你get到了我但願讀者get到的思想:將結果存儲,在合適的時候回調父協程,經過父協程的狀態機從新執行對應狀態的的代碼塊。

我就是經過這樣的思想,實現了幾個協程的流轉,以及執行結果的同步,並且沒有阻塞!

kotlin的協程還有不少我沒提到的強大功能,由於寫本文的想法很簡單:就是想知道kotlin的協程是如何使用閉包實現的。我以爲上文寫的很簡單了,就算表述不行,你跑一下代碼就能恍然大悟!我真的盡力了鐵子們😭,但願大家讀完這一萬多個字能有收穫!

相關文章
相關標籤/搜索