Kotlin(五)掌握基礎知識:協程(kotlinx.coroutines)詳解

Kotlin(一)掌握基礎知識:數據、集合、庫函數
Kotlin(二)掌握基礎知識:字符串和異常處理
Kotlin(三)掌握基礎知識:類
Kotlin(四)掌握基礎知識:線程(Thread.kt)詳解

協程是輕量級的線程,他降低了線程創建,線程切換,線程初始化的性能消耗;
協程具有以下幾個特點

  1. 不是被操作系統內核所管理,而完全是由程序所控制;
  2. 協程在線程中是順序運行的,協程的異步和併發操作是通過協程的掛起方法來執行的,協程掛起時不會阻塞線程;這點不同於線程,線程一旦掛起,該線程就會被阻塞;
  3. 協程運行在線程當中,一個線程中可以創建多個協程,每一個協程可以理解爲一個耗時任務

協程的代碼在 kotlinx.coroutines 中,這個包需要通過dependencies來引入進來;

我們先用IntelliJ IDEA來創建一個工程來練習協程的使用,其步驟如下:

  1. 創建空項目並添加模塊

    1)File -> New -> Project,選擇Empty Project
    在這裏插入圖片描述
    2) 填入項目名稱和項目位置,點擊finish
    在這裏插入圖片描述
    3)在項目結構中選擇Modules,完成模塊添加,此處勾選了Java和Kotlin/JVM表示該模塊會添加java和kotlin相關庫
    在這裏插入圖片描述
    4)點擊Next填入相關信息,點擊OK完成項目的添加
    在這裏插入圖片描述
    5)模塊添加完成後的項目結構如下:
    在這裏插入圖片描述
    其中build.gradle是在編譯會用到的一些庫

  2. 在build.gradle文件中添加協程需要用的庫

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
}
  1. 新建kt文件,就可以正常使用協程了,此處新建了一個test.kt的文件
    在這裏插入圖片描述

現在我們就來充分認識一下協程
  1. 創建協程:通過GlobalScope.launch來創建
fun main() {
    GlobalScope.launch { // 在後臺啓動一個新的協程,協程的生命週期和應用程序生命週期綁定
        delay(1000L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
        println("World!") 
    }
    println("Hello,") // 協程已在等待時主線程還在繼續
    Thread.sleep(2000L) // 阻塞主線程 2 秒鐘
/* 
通過GlobalScope創建的新協程的生命週期受應用程序的生命週期限制,類似java中的守護線程,
所以當主線程運行結束之後,GlobalScope.launch方法創建的協程也就消失了
所以你會發現如果你將Thread.sleep(2000L)這行代碼取消掉,你會發現只打印了Hello
這是因爲主線程打印了Hello之後,主程序退出了,然後GlobalScope.launch啓動的協程需要延遲1秒纔打印,但是隨着主線程的退出,協程也退出了,顧不打印World
*/
}

代碼運行結果

Hello,
World!

  GlobalScope實現了CoroutineScope接口,CoroutineScope接口表示一個協程的構造器,每一個協程的構造器都需要實現該接口,CoroutineScope接口裏麪包含了實現協程的上下文;
  GlobalScope.launch定義在Builders.Common.kt文件中,該文件對CoroutineScope接口實現了一個擴展函數launch();
  協程上下文包含一個 協程調度器 (CoroutineDispatcher)它確定了哪些線程或與線程相對應的協程執行。協程調度器可以將協程限制在一個特定的線程執行,或將它分派到一個線程池,亦或是讓它不受限地運行
  通過GlobalScope創建的新協程的生命週期受應用程序的生命週期限制,類似java中的守護線程,所以當主線程運行結束之後,GlobalScope.launch方法創建的協程也就消失了

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

   launch方法有三個參數,第一個參數context表示協程的上下文運行環境,第二個參數start表示協程的構造器該如何開始這個協程,第三個參數block是一個lambda表達式,表示協程運行主體;
   launch方法方法返回Job實例,通過這個Job實例我們可以判斷這個協程是否完成,取消等等操作,Job有如下幾個關鍵方法:

  1. job.start() :啓動協程
  2. job.join() :等待協程執行完畢
  3. job.cancel() :取消一個協程
  4. job.cancelAndJoin() : 等待協程執行完畢然後再取消

   launch方法的函數體主要是根據start類型來創建一個協程,這個協程是LazyStandaloneCoroutine或者是StandaloneCoroutine,這兩個類均實現了AbstractCoroutine抽象類,AbstractCoroutine纔是真正實現協程的主類;
   第一個參數,即協程的上下文運行環境,它可以被用來顯式的爲一個新協程或其它上下文元素指定一個調度器。

fun main() = runBlocking<Unit> {
    launch { // 運行在父協程的上下文中,即 runBlocking 主協程
        println("main runBlocking      : I'm working in thread 	${Thread.currentThread().name}")
    }
    /* 非受限調度器 vs 受限調度器
Dispatchers.Unconfined 協程調度器在調用它的線程啓動了一個協程,但它僅僅只是運行到第一個掛起點。
掛起後,它恢復線程中的協程,而這完全由被調用的掛起函數來決定。非受限的調度器非常適用於執行不消耗 
CPU 時間的任務,以及不更新侷限於特定線程的任何共享數據(如UI)的協程。*/
    launch(Dispatchers.Unconfined) { //  不受限的: 將工作在主線程中
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // 將會獲取默認調度器
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // 將使它獲得一個新的線程
    /* newSingleThreadContext 爲協程的運行啓動了一個線程。 一個專用的線程是一種非常昂貴的資源。 在真實的應用程序中兩者都必須被釋放,當不再需要的時候,使用 close 函數,或存儲在一個頂層變量中使它在整個應用程序中被重用。*/
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }
}

輸出結果爲:

Unconfined            : I'm working in thread main
Default               : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking      : I'm working in thread main

   創建一個協程之後,這個協程的啓動方法是由第二個參數start來決定的,其在CoroutineStart中定義了四種類型

  • DEFAULT: 默認實現方式,表示創建協程之後立即運行
  • LAZY:延遲啓動協程,如可以通過start方法來運行協程
  • ATOMIC:實現方式類似DEFAULT,區別是這個協程在運行之前不能被取消
  • UNDISPATCHED
  1. 創建協程:通過runBlocking來創建
    創建線程的另一個協程的方式是通過runBlocking函數,其實現方式爲:
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
 ****
}

該函數和launch函數的不同點是會阻塞調用者線程直到協程完成,類似Thread.sleep()

fun main() {
    GlobalScope.launch { // 在後臺啓動一個新的協程並繼續
        delay(1000L)
        println("World!")
    }
    println("Hello,") // 主線程中的代碼會立即執行
    runBlocking {     // 但是這個表達式阻塞了主線程
        delay(5000L)
    }
    println("主線程運行結束")
}

上述代表表現形式爲:先打印hello,然後通過runBlocking啓動了一個協程,該協程會阻塞調用者即主線程,只有當runBlocking 函數體運行結束,即休眠5秒之後,開始打印"主線程運行結束"

  1. 創建協程:GlobalScope.async
    async和launch的區別就是async帶有返回值,其表示異步/併發執行協程;
fun main() {
    runBlocking {
        var deferred = GlobalScope.async {
            println("協程1開始執行")
            return@async "GlobalScope.async 1"
        }
        var deferred2 = GlobalScope.async {
            println("協程2開始執行")
            return@async "GlobalScope.async 2"
        }
        Thread.sleep(2000L)
        if (deferred.isCompleted && deferred2.isCompleted) {
            var result1 = deferred.await();
            var result2 = deferred2.await();
            println("協程執行完畢$result1 + $result2")
        } else {
            println("協程未執行")
        }
    }
}

輸出爲:

協程1開始執行
協程2開始執行
協程執行完畢GlobalScope.async 1 + GlobalScope.async 2

async函數返回一個Deferred類型,該類繼承自Job,並且該類提供了wait函數,getCompleted函數等函數來獲取協程返回值;其中getCompleted函數如果協程任務還沒有執行完成則會拋出IllegalStateException

同理我們可以將async封裝成一個函數

fun main() {
    var deferred = GlobalScope.async {
        asyncValue()
    }
    Thread.sleep(2000L)
    if (deferred.isCompleted) {
        var result = deferred.getCompleted();
        println("協程執行完畢$result")
    } else {
        println("協程未執行")
    }
}

fun asyncValue(): String {
    return "GlobalScope.async"
}

async函數可以通過將 start 參數設置爲 CoroutineStart.LAZY 而變爲惰性的。 在這個模式下,只有結果通過 await 獲取的時候協程纔會啓動,或者在 Job 的 start 函數調用的時候

  1. suspend修飾符
    suspend意爲暫停,表示該方法是一個掛起函數,從而在不阻塞的情況下執行其他工作;同時只有suspend函數才能調用susend函數
import kotlinx.coroutines.*

import kotlinx.coroutines.*
import kotlin.system.*

fun main() = runBlocking<Unit> {
    println("start")
    val time = measureTimeMillis {
        val one = doSomethingUsefulOne()
        val two = doSomethingUsefulTwo()
        println("The answer is ${one + two}")
    }
    println("Completed in $time ms")
}

suspend fun doSomethingUsefulOne(): Int {
    println("doSomethingUsefulOne")
    delay(3000L) // pretend we are doing something useful here
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    println("doSomethingUsefulTwo")
    delay(5000L) // pretend we are doing something useful here, too
    return 29
}

輸出爲:

/*
主協程先打印start,然後打印doSomethingUsefulOne,延遲3秒之後打印doSomethingUsefulTwo,然後再延遲5秒打印The answer is 42
*/
start
doSomethingUsefulOne
doSomethingUsefulTwo
The answer is 42
Completed in 8007 ms

可見通過suspend修飾符修飾的方法兩者之間是沒有依賴,按照順序執行;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章