Kotlin (5): Coroutines



學習Kotlin最重要的項目,就是Coroutines.
這可是之前Java所沒有的,Java的Thread 不好用,這個coroutines才是適合非同步領域的東西.

Coroutines 這個單字是由兩個英文單字合併而成的,分別是 cooperation + routine.用來做非同步和非阻塞任務.意思就是要各個子任務程協作運行的意思,所以明白了它被創造出來是要解決異步問題的。

Kotlin的協程完美復刻了Go語言的協程設計模式( 作用域/channel/select), 將作用域用對象分類; 可以更好地控制作用域生命週期;
await模式(類似JavaScript的異步任務解決方案)
Kotlin參考RxJava響應式框架創造出Flow
使用協程開始就不需要考慮線程的問題, 只需要在不同場景使用不同的調度器就好


github: https://github.com/Kotlin/kotlinx.coroutines

kotlin home: https://kotlinlang.org

reference: https://juejin.cn/post/6987724340775108622


仔細看完文件後,發現coroutines並不是那麼簡單,反而使用方法有點複雜.

----

dependencies

{

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:+'

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:+'

}

-----

coroutine 典型用法,啟動協程:
啟動一個新的協程, 常用的主要有以下幾種方式:它們被稱為coroutine builders. 
launch
async (可從Coroutine 返回值)
runBlocking

----
launch– Launches new coroutine without blocking current thread and returns a reference to the coroutine as a Job. The coroutine is canceled when the resulting job is cancelled.
不會阻塞直到結果返回
不會阻塞線程
並行執行

async– Creates new coroutine and returns its future result as an implementation of Deferred. The running coroutine is canceled when the resulting object is cancelled.
當使用awiat函數時,會阻塞直到結果返回
如果不使用await,其效果與launch一樣
適用於多個並行任務但需要等待結果返回情形
並行執行

withContext:
會阻塞當前協程直到函數返回
從指定的Dispatcher執行函數
當執行函數的時候不會阻塞線程
串行執行

---

launch() 會建立一個 coroutine,然後馬上返回並繼續執行。而且,這個新的 coroutine 會以非同步的方式執行區塊(block)裡面的程式碼。
launch並不會等待當前協程的執行結果,所以launch適用於一些不需要等待返回的函數.

button.setOnClickListener {
setNewText("Click!")

CoroutineScope(IO).launch {
fakeApiRequest()
}
}
Coroutines不只有一種撰寫方式.其他用法:

fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
// ...
): Job

or

suspend fun main() = coroutineScope { launch { delay(5000) println("Kotlin Coroutines World!") } println("Hello") }

output:

Hello 
Kotlin Coroutines World!

----
or

fun main() { GlobalScope.launch{

     // launch a new coroutine in background and continue         delay(1000L) // non-blocking delay for 1 second (default time unit is ms) println("World!") // print after delay

}

println("Hello,")         

    // main thread continues while coroutine is delayed

    Thread.sleep(2000L)

    // block main thread for 2 seconds to keep JVM alive }

ps: 上面這段程式碼的輸出: 先列印Hello, 延遲1s之後, 列印World.

----

來另一個範例:

private fun setNewText(input: String){
val newText = text.text.toString() + "\n$input"
text.text = newText
}

private suspend fun setTextOnMainThread(input: String) {
withContext (Main) {
setNewText(input)
}
}

private suspend fun fakeApiRequest() {
logThread(
"fakeApiRequest")

val result1 = getResult1FromApi() // wait until job is done

if ( result1.equals("Result #1")) {

setTextOnMainThread(
"Got $result1")

val result2 = getResult2FromApi() // wait until job is done

if (result2.equals("Result #2")) {
setTextOnMainThread(
"Got $result2")
}
else {
setTextOnMainThread(
"Couldn't get Result #2")
}
}
else {
setTextOnMainThread(
"Couldn't get Result #1")
}
}

可以很明顯看到,當執行到suspend function. 系統會產生一個全新thread, 在此Function內程式會 block住,不會往下執行. 而同時間另一個執行序:主執行序(UI main thread)會繼續執行.

這邊就有兩個重點了,

withContext (Dispatchers.Main) {

           ...

 }

1. whar is Dispatchers.Main ???

Answer: 

To specify where the coroutines should run, Kotlin provides three dispatchers that you can use:

Dispatchers.Main — Use this dispatcher to run a coroutine on the main Android thread. This should be used only for interacting with the UI and performing quick work. Examples include calling suspend functions, running Android UI framework operations, and updating LiveData objects.

Dispatchers.IO — This dispatcher is optimized to perform disk or network I/O outside of the main thread. Examples include using the Room component, reading from or writing to files, and running any network operations.

Dispatchers.Default — This dispatcher is optimized to perform CPU-intensive work outside of the main thread. Example use cases include sorting a list and parsing JSON.

2.withContext():

withContext可以在scope裡面切換Dispatcher,並在執行完成後自動切回原本的Dispatcher,再接著執行後面的code.


-------
async and await
執行異步network任務,返回結果更新UI
async 會啟動新的協同程式,並透過 await 暫停函式傳回結果。
一般函式無法呼叫 await,因此您通常從一般函式對新協同程式執行 launch 作業。只有在其他協同程式內時,或在暫停函式內,並執行平行分解時,才使用 async。

fun asyncTest1() {
    print("start")
    GlobalScope.launch {
        val deferred: Deferred<String> = async {
       //協程將線程的執行權交出去,該線程繼續要做的事情,到時間後會恢復至此,繼續向下執行
            delay(2000)//2秒無阻塞延遲
            print("asyncOne")
            "HelloWord"//這裡返回值為HelloWord
        }

        //等待async執行完成獲取返回值,此處並不會阻塞線程,而是掛起,將線程的執行權交出去
        //等到async的協程體執行完畢後,會恢復協程繼續往下執行
        val result = deferred.await()
        print("result == $result")
    }
    print("end")
}

----

runBlocking: (很少用)

有別於launch和async讓程式碼並聯執行, runBlocking可以建立一個block當前線程的coroutine. 
所以它主要被用來在main函數中或者測試中使用, 作為連接函數.
一般我們在項目中是不會使用runBlocking, 因為阻塞主線程沒有開啟的任何意義.


fun main() = runBlocking { launch { println("Hello Coroutine!") } println("Complete") }

------

fun main() = runBlocking {
    val job = GlobalScope.launch {
        // launch a new coroutine and keep a reference to its Job
        delay(1000L)
        println("World! + ${Thread.currentThread().name}")
    }
    println("Hello, + ${Thread.currentThread().name}")
    job.join() // wait until child coroutine completes
}

輸出:

Hello, + main @coroutine#1
World! + DefaultDispatcher-worker-1 @coroutine#2

----


----

suspend:

suspend方法只能在協程或者另一個suspend方法中被調用.

----

What is GlobalScope or CoroutineScope ???

直接透過 CoroutineScope 來實現一個自己的協程作用域.
而不建議直接使用 GlobalScope, GlobalScope是一個單例,其生命週期與Android應用生命週期相同,而且並未與Android生命週期組件(Activity、Service等進行關聯),其周期需要自己管理.

----
有使用到MVVM架構的會多了這個 viewModelScope: 
viewModelScope means less boilerplate code.
AndroidX lifecycle v2.1.0 introduced the extension property viewModelScope to the ViewModel class.

fun login(username: String, token: String) { // Create a new coroutine on the UI thread viewModelScope.launch {

val jsonBody = "{ username: \"$username\", token: \"$token\"}" // Make the network call and suspend execution until it finishes val result = loginRepository.makeLoginRequest(jsonBody) // Display result of the network request to the user when (result) { is Result.Success<LoginResponse> -> // Happy path else -> // Show error in UI } } }


------


Job 型態的物件:

我們也可以先等待新的 coroutine 執行結束後再繼續,如下面的程式碼。

fun main() = runBlocking { val job = launch { println("Hello Coroutine!") } job.join() println("Complete") }

launch() 會回傳一個 Job 型態的物件,而這個 Job 就是代表 coroutine 了。之後,呼叫 Job.join()當前 runBlocking() 的 coroutine 會被暫停(suspend),等待 job 的 coroutine 執行結束後,再繼續執行。當前的 coroutine 被暫停後會釋放當前的 thread,所以當前的 thread 可以去執行其他的 coroutine。

----

 job.cancel()

通過調用 job.cancel() 方法,可以取消該job下所有正在進行的任務。

---
coroutine 總結大概就是上面,是不是有點複雜呢?
但別擔心,其實只要基本的用法會,就可以了.
coroutine還有很多進階用法沒有說明完,可能之後再慢慢補上.

留言

這個網誌中的熱門文章

最爛的銀行服務-玉山銀行

SMR疊瓦式hdd致命缺陷被解決????!!!

Mark App Design Apps - Terms and Privacy Policy (服務條款,隱私權政策)

更改google drive預設存放目錄位置Change Google Drive Default Folder Location in Windows

ios app 上架時app icon要注意事項

Gmail 永久刪除大型電子郵件

舊有app在Android 12 閃退問題& app Splash screens

app bundle and bundletool. 關於aab安裝問題

關於google play console app應用程式簽署

google play 正式發布前測試報告...非常好用.