探究高級的Kotlin Coroutines知識

来源:https://www.cnblogs.com/mengdd/archive/2019/02/12/deep-explore-kotlin-coroutines.html
-Advertisement-
Play Games

要說程式如何從簡單走向複雜, 線程的引入必然功不可沒, 當我們期望利用線程來提升程式效能的過程中, 處理線程的方式也發生了從原始時代向科技時代發生了一步一步的進化, 正如我們的Elisha大神所著文章[The Evolution of Android Network Access](https://... ...


要說程式如何從簡單走向複雜, 線程的引入必然功不可沒, 當我們期望利用線程來提升程式效能的過程中, 處理線程的方式也發生了從原始時代向科技時代發生了一步一步的進化, 正如我們的Elisha大神所著文章The Evolution of Android Network Access中所講到的, Future可能會是Kotlin Coroutines的時代.

什麼是Coroutines

Coroutines是Kotlin 1.1推出的實驗性的一個擴展, 它被定義為一個輕量級的高效的線程框架, 並且在1.3版本正式發佈, 去掉Experiment標簽.

如何啟動一個Coroutines

最基礎的創建一個Coroutines的方法就是使用launch或者async, 二者的區別是前者返回的是一個Job, 不帶結果 而後者可以將結果以Deferred<T>格式返回.

如:

val job = launch {
    delay(100)
}

而通常在Coroutines內執行的函數都會有一個suspend聲明, 而有suspend聲明的函數也只能在Coroutines Scope中調用.

suspend的意思是這個函數可以被suspend(掛起), 讓Coroutines來調度它, 這也是為何Kotlin的delay函數可以不阻塞的進行延遲, 因為它就是一個suspend函數.

Coroutines與線程的關係

Coroutines可以簡單理解為一個有隊列的任務鏈, 每一個Coroutines都有自己的Context, 而Context又可以決定其運行的線程.

所以可以看到, 並不是起一個Coroutines就是起了一個線程, 而只是啟動了一個在某個Scope下運行的協程(Coroutines)罷了. 這裡的Scope (CoroutineScope) 內部包含了一個 Context (CoroutineContext).

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

如果只是通過launch來啟動一個協程, 那它將會運行在Parent Scope所定義的線程中, 但是如果使用GlobalScope.launch來啟動一個協程, 它將會使用線程池中的線程來創建一個協程, 線程池的大小跟CPU的核數相關.

當然launch也支持自己傳入一個CoroutinesContext來控制它運行的線程, 它叫做CoroutineDispatcher, 是Context的子類.

上面講了預設的launch會啟在父Scope(Context)的線程中, 而launch(Dispatchers.Default)則等於GlobalScope.launch, 還可以通過launch(newSingleThreadContext("MyOwnThread"))來啟動自己的線程, 另外有一個不推薦在general code中出現的launch(Dispatchers.Unconfined), 它將會運行在第一個進入Suspend狀態的線程中.

可以舉一個簡單的例子:

val job = launch {
    log("hehe")
    delay(1000)
    log("haha")
}

這個協程是可以完全在main函數里執行完的, 即輸出結果為:

hehe
haha

因為launch會跑在main的scope中. 如果替換成:

val job = GlobalScope.launch {
    log("hehe")
    delay(1000)
    log("haha")
}

則只會輸出hehe, 因為主線程已經結束.

這裡我們可以通過job.join()來等待子協程執行結束, 這一點跟大家熟知的線程的join是一樣.

如何切換Context

如果把Context對應到我們平時認為的線程, 那麼這個問題可以類比成 如何切換線程.

答案是使用withContext, 舉一個簡單的慄子.

launch(UI) {
    updateUI()
    val result = withContext(IO) {
        
    }
    setView(result)
}

它類似於async(IO){ }.await().

如何共用資源

線程與線程之間會涉及到同步與資源競爭的關係, 協程亦是如此.

通常情況下線上程中我們解決問題的方式是加鎖, 而不正確的使用可能會導致性能下降甚至死鎖(dead lock. 或者在高級語言中使用已經實現線程安全的數據類型, 來進行誇線程操作。

而我們的Coroutines自然也考慮到了這一點, 它認為我們不應該以共用資源來進行通信, 而是以通信來進行資源共用.

Do not communicate by sharing memory; instead, share memory by communicating.

所以它提出了一個叫做Channel的東西來在不同的Coroutines之間進行通信.

譬如我們期望將一堆數據交給兩個並行的協程進行處理, 那麼我們可以把數據放進Channel, 其他的協程從這個Channel進行數據讀取.

launch {
    for (o in data) { channel.send(o) }
    channel.close()
}

launch(One) {
    for (o in channel) {
        xxx
    }
}

launch(Two) {
    for (o in channel) {
        xxx
    }
}

一定要記得關閉channel, 否則從channel讀取數據的協程都將會無限掛起等待數據傳過來.

由於Channel本身實現了iterator, 所以直接通過in就可以挨個取出內部的數據.

ReceiveChannel與SendChannel

上一個環節提到的協程之間是通過Channel來進行通信, 而Channel本身卻是實現了接收管道與發送管道兩個介面.

我們可以通過producer函數來進行生成數據, 提供給別的協程, 因為它的返回值是一個ReceiveChannel.

val channel = produce<XXX>() {
    for (o in data) send(o)
}

而且produce自己會做channel close的處理, 省去我們發送完畢還要掉close的煩惱.

如果我們多個協程需要發送請求並集中處理, 或者可以叫數據整合, 那麼我們可能需要用到actor這個函數, 它的返回值是一個SendChannel.

val channel = actor<XXX>() {
                consumeEach {
                   xxx     
                }
            }

launch(One) {
    channel.send(xxx)
}
launch(Two) {
    channel.send(xxx)
}

由於actor返回的SendChannel有點像是一個郵箱, 它會不斷的接收數據, 所以必須手動關閉才會停止.

多個Channel之間數據如何進行選擇

Coroutines推出一個仍在Experiment階段的關鍵字select來在多個suspend function中進行選擇第一個到達available的, 其實有點像RxJava的concat+first.

比如我有兩個接收Channel, 但是每一個Channel接收到數據的頻率不得而知, 我想分別從中得到數據, 這裡就需要使用select.

select<Unit> {
    channel1.onReceive {}
    channel2.onReceive {}
}

如果在配合外圍的迴圈, 就可以做到不斷的去接收兩個Channel的數據.

再比如有兩個發送Channel都可以處理我的需求, 我也不知道這個時候誰是空閑的, 那也可以通過select來解決.

select<Unit> {
    channel1.onSend(xxx) {}
    channel2.onSend(xxx) {}
}

有時候兩個Channel是嵌套使用的.

比如一個咖啡店, 他們會不斷的收到Oder, 只有兩個打咖啡的服務員, 咖啡機也只有兩個口, 如果我們對這個咖啡店進行抽象. 將Oder存在於一個Channel里, 服務員接收Order並不斷的把咖啡遞出來, 這也是一個Channel, 咖啡機會不斷接收到服務員需要打咖啡的操作, 也這是一個Channel.

而在這個過程中, 兩個服務員會有一個選擇, 咖啡機的兩個出口也會有一個選擇的過程.

如果抽象成我們的Coroutines代碼, 或許會是這個樣子:

val orderChannel = producer {
    for (o in orders) send(o)
}

val waiter1 = producer {
    for (o in orderChannel) { 
        pullCoffee(o)
    }
}

// waiter2 is the same as 1

val coffeePort1 = actor {
    consumeEach { 
        //pass coffee through channel inside order
        it.channel.send(Coffee)
        it.channel.close()
    }
}

// coffeePort2 is the same as 2

pullCoffee {
    select<Coffee> {
        coffeePort1.onSend(Request(channel)) {
            //get coffee from coffeePort
            channel.recevie()
        }
        coffeePort2.onSend ....
    }
}

while(someCondition) {
    select<Coffee> {
        waiter1.onReceiveOrNull {
            //上菜了
        }
        waiter2.onReceiveOrNull {
            //上菜了
        }
    }
}

補充說明

協程作為未來non blocking編程的方向, 需要大家花時間去理解, 花時間去嘗試, 在此特別推薦這個咖啡小程式幫助大家學習.

https://medium.com/@jagsaund/kotlin-coroutines-channels-csp-android-db441400965f

以及官方的Overview

https://kotlinlang.org/docs/reference/coroutines-overview.html

還有個CheatSheet可以參考

https://blog.kotlin-academy.com/kotlin-coroutines-cheat-sheet-8cf1e284dc35


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 當需要周期性的去執行一個方法時,我們可以先寫好方法,然後交給資料庫去完成就可以的。 步驟:首先打開SQL資料庫中SQLServer代理--》右鍵作業--》新建作業: 如果SQL Server代理被禁用了,那就郵件啟動代理即可; 或者這樣: 選擇電腦——>設備管理——>服務與應用程式——>服務——> ...
  • Linux中安裝完Oracle後,預設的 上下鍵是不能用的,安裝了 之後就能通過上下鍵翻回歷史命令了 1. 下載地址 "https://github.com/hanslub42/rlwrap/releases" 2. 安裝步驟 安裝readline 及 依賴 上傳、解壓、編譯、安裝rlwrap 配置 ...
  • 一、下載安裝 MySQL是一個關係型資料庫管理系統,由瑞典MySQL AB 公司開發,目前屬於 Oracle 旗下公司。MySQL 最流行的關係型資料庫管理系統,在 WEB 應用方面MySQL是最好的 RDBMS (Relational Database Management System,關係數據 ...
  • 禁用觸發器: 啟用觸發器: ...
  • SQLite資料庫簡介 SQLite 是一個輕量級資料庫,它是D. Richard Hipp建立的公有領域項目,在2000年發佈了第一個版本。它的設計目標是嵌入式的,而且占用資源非常低,在記憶體中只需要占用幾百kB的存儲空間,這也是Android移動設備採用SQLite資料庫的重要原因之一。 SQLi ...
  • 前言 工欲善其事,必先利其器 所以第一篇我們來說說 Flutter 環境的搭建。 筆者這邊使用的是 MAC 電腦,因此以 MAC 電腦的環境搭建為例。 Windows 或者 Linux 也是類似的操作。 Flutter 有英文版的官網和中文網,大家可以根據自己的喜好和情況進行選擇。 點擊下麵的鏈接可 ...
  • 現在 Android 開發免不了要和 Gradle 打交道,所有的 Android 開發肯定都知道這麼在 中添加依賴,或者添加配置批量打包,但是真正理解這些腳本的人恐怕很少。其實 Gradle 的 可以說是一個代碼文件,熟悉 Java 的人理解起來很簡單的,之所以不願意去涉及,主要感覺沒有必要去研究 ...
  • 1:時間戳轉NSDate NSString *timeStamp =@"1545965436"; NSDate *date = [NSDate dateWithTimeIntervalSince1970:[timeStamp intValue]]; ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...