【Kotlin】協程

little_fat_sheep發表於2024-12-07

1 前言

​ 相較於 C# 中的協程(詳見 → 【Unity3D】協同程式),Kotlin 中協程更靈活,難度更大。

​ 協程是一種併發設計模式,用於簡化非同步程式設計,它允許以順序化的方式表達非同步操作,避免回撥地獄等問題。使用協程,可以將非同步操作的程式碼像同步程式碼一樣寫,而無需顯式地管理執行緒。

​ 在 Kotlin 中,協程由 kotlinx.coroutines 庫提供支援。它使用 suspend 修飾符來標記掛起函式(即可暫停執行並稍後恢復執行的函式),這使得編寫非同步程式碼更加直觀和簡單。

​ 協程和執行緒具有以下異同點。

1)併發模型

  • 執行緒:執行緒是作業系統提供的執行單位,一個程序可以擁有多個執行緒,執行緒之間相對獨立,資料共享需要透過特殊手段(如鎖)保證安全。
  • 協程:協程是一種使用者態的輕量級執行緒,由開發者控制其執行與暫停,可以在同一執行緒上併發執行,透過掛起和恢復的方式,實現非阻塞的併發。

2)資源消耗

  • 執行緒:每個執行緒都需要分配一定的記憶體和系統資源,執行緒切換時會有一定的開銷。
  • 協程:協程是使用者級的,由協程排程器(Coroutine Dispatcher)排程,通常會複用較少的系統資源,因此更輕量級。

3)程式設計模型

  • 執行緒:多執行緒程式設計通常以共享狀態和鎖為基礎,編寫併發程式碼較為複雜。
  • 協程:協程提供了一種結構化併發程式設計的方式,透過掛起函式的呼叫實現程式碼的暫停和恢復,使得非同步程式設計更易於理解和維護。

4)錯誤處理

  • 執行緒:多執行緒程式設計中,錯誤處理相對困難,需要開發者手動處理異常和執行緒間的通訊。
  • 協程:協程提供了更加簡單和一致的錯誤處理方式,透過結構化的異常處理機制,可以輕鬆處理協程中的異常。

5)效能

  • 執行緒:建立和管理執行緒可能會帶來較大的開銷,尤其是在大量執行緒同時執行時,執行緒切換的開銷也會比較高。
  • 協程:協程由於是輕量級的使用者級執行緒,資源消耗較少,因此在大規模併發場景下可能表現更優。

​ 總的來說,協程相比於傳統的執行緒模型,更加靈活、輕量級,並且提供了更加簡單和結構化的併發程式設計方式,使得非同步程式設計更加容易和優雅。

2 協程相關類圖

img

3 協程原始碼

3.1 協程作用域原始碼(CoroutinueScope)

​ 協程的作用域定義了協程的作用域範圍,當該作用域被銷燬時,其中的協程也會被取消。協程的作用閾主要有 CoroutineScope、MainScope、GlobalScope、lifecycleScope 、viewModelScope,主要區別如下。

  • CoroutineScope:CoroutineScope 是通用的協程作用域,用於定義協程的作用域範圍,當該作用域被銷燬時,其中的協程也會被取消。
  • MainScope:MainScope 是 Kotlin 中提供的特定於 Android 的協程作用域,用於在 Android 主執行緒上啟動協程,通常在 Android 的 Activity 或 Fragment 中使用 MainScope,以確保在主執行緒上執行協程,並在相關生命週期結束時取消協程。
  • GlobalScope:GlobalScope 是 Kotlin 中提供的一個全域性協程作用域,它是一個頂層物件,使用者可以在任何地方使用 GlobalScope 啟動協程,但不推薦在 Android 中使用它,因為它的生命週期很長,並且不受管理,可能導致記憶體洩漏等問題。
  • lifecycleScope:lifecycleScope 是 Android Jetpack 中的 Lifecycle 模組提供的一個擴充套件屬性,它的生命週期與相關的元件(如 Activity 或 Fragment)的生命週期繫結,從而避免記憶體洩漏等問題。
  • viewModelScope:viewModelScope 是 Android Jetpack 中 Lifecycle 模組提供的一個擴充套件屬性,它的生命週期與 ViewModel 的生命週期繫結,從而避免記憶體洩漏等問題。

3.1.1 CoroutineScope

public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

​ 說明:CoroutineScope 是通用的協程作用域,用於定義協程的作用域範圍,當該作用域被銷燬時,其中的協程也會被取消。

3.1.2 MainScope

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

​ 說明:MainScope 是 Kotlin 中提供的特定於 Android 的協程作用域,用於在 Android 主執行緒上啟動協程,通常在 Android 的 Activity 或 Fragment 中使用 MainScope,以確保在主執行緒上執行協程,並在相關生命週期結束時取消協程。

3.1.3 GlobalScope

public object GlobalScope : CoroutineScope

​ 說明:GlobalScope 是 Kotlin 中提供的一個全域性協程作用域,它是一個頂層物件,使用者可以在任何地方使用 GlobalScope 啟動協程,但不推薦在 Android 中使用它,因為它的生命週期很長,並且不受管理,可能導致記憶體洩漏等問題。GlobalScope 是一個單例,其作用域的生命週期跟隨應用程式的生命週期,中間不能取消(cancel)。

3.1.4 lifecycleScope

public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

// -----------------------------------------------------------
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

// -----------------------------------------------------------
internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver

// -----------------------------------------------------------
public abstract class LifecycleCoroutineScope internal constructor() : CoroutineScope

​ 說明:lifecycleScope 是 Android Jetpack 中的 Lifecycle 模組提供的一個擴充套件屬性,它的生命週期與相關的元件(如 Activity 或 Fragment)的生命週期繫結,從而避免記憶體洩漏等問題。

​ 使用 lifecycleScope 時,需要在 build.gradle 中引入以下依賴。

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"

​ 並匯入包名。

import androidx.lifecycle.lifecycleScope

​ AppCompatActivity、FragmentActivity 與 LifecycleOwner 存在以下繼承關係。因此可以在 AppCompatActivity 和 FragmentActivity 中直接訪問 lifecycleScope。

AppCompatActivity → FragmentActivity → ComponentActivity → LifecycleOwner

3.1.5 viewModelScope

public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

// --------------------------------------------------------------------------
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}

​ 說明:viewModelScope 是 Android Jetpack 中 Lifecycle 模組提供的一個擴充套件屬性,它的生命週期與 ViewModel 的生命週期繫結,從而避免記憶體洩漏等問題。

​ 使用 viewModelScope 時,需要在 build.gradle 中引入以下依賴。

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'

​ 並匯入包名。

import androidx.lifecycle.viewModelScope

3.2 協程排程器原始碼(Dispatchers)

public actual object Dispatchers {
    // 執行緒池, 適合執行CPU密集型任務(大量佔用量CPU的任務)
    public actual val Default: CoroutineDispatcher = DefaultScheduler
    // Android中是UI執行緒, Swing中是invokerLater執行緒
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
    // 在當前執行緒上執行
    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
    // 執行緒池, 適合執行磁碟讀寫、網路IO、資料庫操作等任務
    public val IO: CoroutineDispatcher = DefaultIoScheduler
    // ...
}

3.3 協程啟動方式原始碼

​ 協程的啟動方式主要有 launch、async、runBlocking、withContext,它們的區別如下。

  • launch:launch 用於啟動一個新的協程,並返回一個 Job 物件,該物件代表了這個新協程;啟動的協程在後臺執行,不會阻塞當前執行緒的執行,並且不會返回協程的執行結果。
  • async:async 用於啟動一個新的協程,並返回一個 Deferred 物件,它是 Job 的子類,可以透過 await 函式獲取協程的執行結果;啟動的協程在後臺執行,不會阻塞當前執行緒的執行。
  • runBlocking:runBlocking 是一個頂層函式,用於啟動一個新的協程並阻塞當前執行緒,直到協程執行完成; runBlocking 本質上是為了在頂層(如 main 函式)使用協程,以及在測試中使用協程;在生產程式碼中不推薦使用 runBlocking,因為它會阻塞當前執行緒,可能導致效能問題。
  • withContext:withContext 用於切換協程的上下文,它會建立一個新的協程並在指定的上下文中執行,它會掛起原來的協程,待新協程執行結束後才恢復執行。

3.3.1 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 用於啟動一個新的協程,並返回一個 Job 物件,該物件代表了這個新協程;啟動的協程在後臺執行,不會阻塞當前執行緒的執行,並且不會返回協程的執行結果。

3.3.2 async

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

​ 說明:async 用於啟動一個新的協程,並返回一個 Deferred 物件,它是 Job 的子類,可以透過 await 函式獲取協程的執行結果;啟動的協程在後臺執行,不會阻塞當前執行緒的執行。

3.3.3 runBlocking

​ runBlocking 官方介紹見 → runBlocking

public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {
    ...
    val currentThread = Thread.currentThread()
    val contextInterceptor = context[ContinuationInterceptor]
    val eventLoop: EventLoop?
    val newContext: CoroutineContext
    if (contextInterceptor == null) {
        // 如果沒有指定排程器(dispatcher), 則建立或使用私有事件迴圈(eventLoop)
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
        eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
            ?: ThreadLocalEventLoop.currentOrNull()
        newContext = GlobalScope.newCoroutineContext(context)
    }
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

​ 說明:runBlocking 是一個頂層函式,用於啟動一個新的協程並阻塞當前執行緒,直到協程執行完成; runBlocking 本質上是為了在頂層(如 main 函式)使用協程,以及在測試中使用協程;在生產程式碼中不推薦使用 runBlocking,因為它會阻塞當前執行緒,可能導致效能問題。

3.3.4 withContext

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
    // ...
    return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
        val oldContext = uCont.context
        val newContext = oldContext.newCoroutineContext(context)
        newContext.ensureActive()
        if (newContext === oldContext) {
            val coroutine = ScopeCoroutine(newContext, uCont)
            return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
        }
        if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
            val coroutine = UndispatchedCoroutine(newContext, uCont)
            withCoroutineContext(newContext, null) {
                return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
            }
        }
        val coroutine = DispatchedCoroutine(newContext, uCont)
        block.startCoroutineCancellable(coroutine, coroutine)
        coroutine.getResult()
    }
}

​ 說明:withContext 用於切換協程的上下文,它會建立一個新的協程並在指定的上下文中執行,它會掛起原來的協程,待新協程執行結束後才恢復執行。

3.4 協程啟動模式原始碼(CoroutineStart)

public enum class CoroutineStart {
	// 立即執行協程體
    DEFAULT,
    // 只有在需要的情況下執行, 需要呼叫job.start()函式才啟動協程
    LAZY,
    // 立即執行協程體, 但在開始執行前無法取消
    ATOMIC,
    // 立即在當前執行緒執行協程體, 直到第一個suspend函式呼叫(啟動較快)
    UNDISPATCHED;
    // ...
}

4 協程應用

4.1 協程作用域應用

4.1.1 CoroutineScope

fun main() {
    println("main-start")
    CoroutineScope(Dispatchers.Default).launch {
        for (i in 1..2) {
            println("CoroutineScope-A-$i")
            delay(100)
        }
    }
    CoroutineScope(Dispatchers.IO).launch {
        for (i in 1..2) {
            println("CoroutineScope-B-$i")
            delay(100)
        }
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

​ 列印如下。

main-start
main-end
CoroutineScope-A-1
CoroutineScope-B-1
CoroutineScope-A-2
CoroutineScope-B-2

​ 說明:結果表明 main、CoroutineScope-A、CoroutineScope-B 並行。

4.1.2 MainScope

fun main() {
    println("main-start")
    MainScope().launch(Dispatchers.Default) {
        test("MainScope-A")
    }
    MainScope().launch(Dispatchers.IO) {
        test("MainScope-B")
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

suspend fun test(tag: String) {
    for (i in 1..2) {
        println("$tag-$i")
        delay(100)
    }
}

​ 列印如下。

main-start
main-end
MainScope-B-1
MainScope-A-1
MainScope-A-2
MainScope-B-2

​ 說明:結果表明 main、MainScope-A、MainScope-B 並行。

4.1.3 GlobalScope

fun main() {
    println("main-start")
    GlobalScope.launch(Dispatchers.Default, CoroutineStart.DEFAULT) {
        test("GlobalScope-A")
        test("GlobalScope-B")
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

suspend fun test(tag: String) {
    for (i in 1..2) {
        println("$tag-$i")
        delay(100)
    }
}

​ 列印如下。

main-start
main-end
GlobalScope-A-1
GlobalScope-A-2
GlobalScope-B-1
GlobalScope-B-2

​ 說明:結果表明 main 與 GlobalScope 並行。

4.1.4 lifecycleScope

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch

class MyActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        lifecycleScope.launch {
            println("lifecycleScope")
        }
    }
}

​ 說明:使用 lifecycleScope 時,需要在 build.gradle 中引入以下依賴。

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"

4.1.5 viewModelScope

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            println("viewModelScope")
        }
    }
}

​ 說明:使用 viewModelScope 時,需要在 build.gradle 中引入以下依賴。

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'

4.1.6 子協程

fun main() {
    println("main-start")
    CoroutineScope(Dispatchers.Default).launch {
        test("CoroutineScope-A")
        launch(Dispatchers.Default) { // 也可以透過async啟動子協程
            test("CoroutineScope-B")
        }
        launch(Dispatchers.Default) { // 也可以透過async啟動子協程
            test("CoroutineScope-C")
        }
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

suspend fun test(tag: String) {
    for (i in 1..2) {
        println("$tag-$i")
        delay(100)
    }
}

​ 列印如下。

main-start
main-end
CoroutineScope-A-1
CoroutineScope-A-2
CoroutineScope-B-1
CoroutineScope-C-1
CoroutineScope-B-2
CoroutineScope-C-2

​ 說明:結果表明 main 與 CoroutineScope-A 並行,CoroutineScope-A 執行結束後,又啟動了 GlobalScope-B、CoroutineScope-C 兩個子協程,它們又並行。

4.2 協程啟動方式應用

4.2.1 launch

fun main() {
    println("main-start")
    MainScope().launch(Dispatchers.Default, CoroutineStart.DEFAULT) {
        test("MainScope")
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

suspend fun test(tag: String) {
    for (i in 1..2) {
        println("$tag-$i")
        delay(100)
    }
}

​ 列印如下。

main-start
main-end
MainScope-1
MainScope-2

4.2.2 async

fun main() {
    println("main-start")
    MainScope().launch(Dispatchers.Default) {
        var deferred = async { // 啟動子協程
            test("MainScope")
            "async return value"
        }
        println("MainScope-xxx")
        var res = deferred.await() // 獲取子協程的返回值, 此處會掛起當前協程, 直到子協程執行完成
        println(res)
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

suspend fun test(tag: String) {
    for (i in 1..2) {
        println("$tag-$i")
        delay(100)
    }
}

​ 列印如下。

main-start
main-end
MainScope-xxx
MainScope-1
MainScope-2
async return value

​ 說明:結果表明 deferred.await() 會掛起當前協程(MainScope),直到子協程(async)執行完成。

4.2.3 runBlocking

fun main() {
    println("main-start")
    runBlocking {
        var deferred = async { // 啟動子協程
            test("runBlocking")
            "async return value"
        }
        launch { // 啟動子協程
            var res = deferred.await() // 獲取子協程的返回值, 此處會掛起當前協程, 直到子協程執行完成
            println(res)
        }
        println("runBlocking-xxx")
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

suspend fun test(tag: String) {
    for (i in 1..2) {
        println("$tag-$i")
        delay(100)
    }
}

​ 列印如下。

main-start
runBlocking-xxx
runBlocking-1
runBlocking-2
async return value
main-end

​ 說明:結果表明 runBlocking 啟動了一個新的協程(runBlocking),並阻塞了當前執行緒(main),直到協程執行完成;deferred.await() 會掛起當前子協程(async),直到子協程(launch)執行完成。

4.2.4 withContext

1)不使用 withContext 返回值

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    println("main-start")
    runBlocking(Dispatchers.IO) {
        println("context1=${coroutineContext[CoroutineDispatcher]}")
        withContext(Dispatchers.Default) { // 啟動子協程, 並掛起當前協程
            println("context2=${coroutineContext[CoroutineDispatcher]}")
            test("withContext")
        }
        println("runBlocking-xxx")
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

suspend fun test(tag: String) {
    for (i in 1..2) {
        println("$tag-$i")
        delay(100)
    }
}

​ 列印如下。

main-start
context1=Dispatchers.IO
context2=Dispatchers.Default
withContext-1
withContext-2
runBlocking-xxx
main-end

​ 說明:結果表明 withContext 建立了子協程,並掛起了 runBlocking 協程,直到 withContext 協程執行完畢才恢復執行。

2)使用 withContext 返回值

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    println("main-start")
    runBlocking(Dispatchers.IO) {
        println("context1=${coroutineContext[CoroutineDispatcher]}")
        var res = withContext(Dispatchers.Default) { // 啟動子協程, 並掛起當前協程
            println("context2=${coroutineContext[CoroutineDispatcher]}")
            "withContext return value"
        }
        println("res=$res")
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

​ 列印如下。

main-start
context1=Dispatchers.IO
context2=Dispatchers.Default
res=withContext return value
main-end

4.3 Job 應用

​ Job 狀態流程轉換如下。(圖片來自 Job.kt 原始碼)

img

4.3.1 start

fun main() {
    println("main-start")
    var job = MainScope().launch(Dispatchers.Default, CoroutineStart.LAZY) {
        test("MainScope")
    }
    job.start() // 註釋該行, job不會執行, test中日誌將不會列印
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

suspend fun test(tag: String) {
    for (i in 1..2) {
        println("$tag-$i")
        delay(100)
    }
}

​ 列印如下。

main-start
main-end
MainScope-1
MainScope-2

​ 說明:註釋掉 job.start(),job 不會執行,test 中日誌將不會列印。

4.3.2 cancel

fun main() {
    println("main-start")
    var job = CoroutineScope(Dispatchers.Default).launch {
        test("CoroutineScope")
    }
    job.cancel()
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

suspend fun test(tag: String) {
    for (i in 1..2) {
        println("$tag-$i")
        delay(100)
    }
}

​ 列印如下。

main-start
main-end
CoroutineScope-1

​ 說明:CoroutineScope-2 未列印出來,因為協程執行到一半被取消了。

4.3.3 join

fun main() {
    println("main-start")
    var job = CoroutineScope(Dispatchers.Default).launch {
        test("CoroutineScope")
    }
    MainScope().launch(Dispatchers.Default) {
        println("MainScope-xxx")
        job.join() // 掛起當前協程, 直到job執行完成
        test("MainScope")
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

suspend fun test(tag: String) {
    for (i in 1..2) {
        println("$tag-$i")
        delay(100)
    }
}

​ 列印如下。

main-start
main-end
MainScope-xxx
CoroutineScope-1
CoroutineScope-2
MainScope-1
MainScope-2

​ 說明:結果表明 job.join() 掛起了 MainScope 協程,直到 CoroutineScope 協程執行完畢才恢復執行。

4.4 異常處理應用

4.4.1 try-catch 處理異常

fun main() {
    println("main-start")
    CoroutineScope(Dispatchers.IO).launch {
        try {
            var a = 1 / 0
        } catch (e: Exception) {
            println(e)
        }
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

​ 列印如下。

main-start
main-end
java.lang.ArithmeticException: / by zero

4.4.2 CoroutineExceptionHandler 處理異常

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    println("main-start")
    var exceptionHandler = CoroutineExceptionHandler { context, throwable ->
        println("context=${context[CoroutineDispatcher]}, message=${throwable}")
    }
    CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
        var a = 1 / 0
    }
    println("main-end")
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

​ 列印如下。

main-start
main-end
context=Dispatchers.IO, message=java.lang.ArithmeticException: / by zero

5 協程併發安全

5.1 不安全的併發訪問

fun main() {
    var count = 0
    CoroutineScope(Dispatchers.Default).launch {
        var jobList = List(1000) { // 建立1000個子協程
            CoroutineScope(Dispatchers.Default).launch {
                count++
            }
        }
        jobList.joinAll() // 掛起當前協程, 直到所有子協程執行完成
        println(count) // 期望列印1000, 但每次執行結果不一樣, 如:990、981、995等
    }
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

5.2 安全的併發訪問

​ 安全的併發訪問工具主要有 Atomic、Mutex、Semaphore、Channel。

  • Atomic:原子操作,主要介面:getAndIncrement、getAndDecrement、getAndAdd、getAndAccumulate、incrementAndGet、decrementAndGet、addAndGet、accumulateAndGet 等。
  • Mutex:輕量級鎖,主要介面:withLock 等。
  • Semaphore:輕量級訊號量,主要介面:withPermit 等。
  • Channel:併發安全的訊息通道,主要介面:send、receive。

5.2.1 Atomic

​ 使用 Java 提供的原子操作型別資料,如:AtomicBoolean、AtomicInteger、AtomicLong、AtomicIntegerArray、AtomicLongArray、AtomicReference、AtomicReferenceArray,可以解決一些併發安全訪問的問題。

fun main() {
    var count = AtomicInteger()
    CoroutineScope(Dispatchers.Default).launch {
        var jobList = List(1000) { // 建立1000個子協程
            CoroutineScope(Dispatchers.Default).launch {
                count.getAndIncrement()
            }
        }
        jobList.joinAll() // 掛起當前協程, 直到所有子協程執行完成
        println(count.get()) // 列印: 1000
    }
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

5.2.2 Mutex

​ Mutex 是輕量級鎖,它的 lock 和 unlock 從語義上與執行緒鎖比較類似,之所以輕量是因為它在獲取不到鎖時不會阻塞執行緒,而是掛起等待鎖的釋放。

fun main() {
    var count = 0
    var mutex = Mutex()
    CoroutineScope(Dispatchers.Default).launch {
        var jobList = List(1000) { // 建立1000個子協程
            CoroutineScope(Dispatchers.Default).launch {
                mutex.withLock {
                    count++
                }
            }
        }
        jobList.joinAll() // 掛起當前協程, 直到所有子協程執行完成
        println(count) // 列印: 1000
    }
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

5.2.3 Semaphore

​ Semaphore 是輕量級訊號量,訊號可以有多個,協程在獲取到訊號後即可執行併發操作。

fun main() {
    var count = 0
    var semaphore = Semaphore(1) // 建立一個訊號量, 裡面只有一個訊號
    CoroutineScope(Dispatchers.Default).launch {
        var jobList = List(1000) { // 建立1000個子協程
            CoroutineScope(Dispatchers.Default).launch {
                semaphore.withPermit {
                    count++
                }
            }
        }
        jobList.joinAll() // 掛起當前協程, 直到所有子協程執行完成
        println(count) // 列印: 1000
    }
    Thread.sleep(1000) // 阻塞當前執行緒, 避免程式過早結束, 協程提前取消
}

​ 說明:Semaphore 的入參表示訊號個數,當 Semaphore 的引數為 1 時, 效果等價與 Mutex。

6 載入網路圖片案例

​ build.gradle 中需要引入以下依賴。

dependencies {
    implementation 'com.github.bumptech.glide:glide:4.12.0'
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
    ...
}

​ AndroidManifest.xml 中需要配置以下許可權。

<uses-permission android:name="android.permission.INTERNET" />

​ MainActivity.kt

package com.zhyan8.kotlinStudy

import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity: AppCompatActivity() {
    private lateinit var imageView: ImageView
    private lateinit var button: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        imageView = findViewById(R.id.imageView)
        button = findViewById(R.id.btn_back)
        button.setOnClickListener{
            lifecycleScope.launch(Dispatchers.IO) {
                loadImageFromUrl("https://images.cnblogs.com/cnblogs_com/blogs/787006/galleries/2393602/o_240421081243_g0001.jpg")
            }
        }
    }

    private suspend fun loadImageFromUrl(url: String) {
        val bitmap = Glide.with(this@MainActivity)
            .asBitmap()
            .load(url)
            .submit()
            .get()
        withContext(Dispatchers.Main) {
            imageView.visibility = View.VISIBLE
            button.visibility = View.GONE
            imageView.setImageBitmap(bitmap)
        }
    }
}

​ activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:gravity="center">

   <ImageView
       android:id="@+id/imageView"
       android:layout_height="match_parent"
       android:layout_width="match_parent"
       android:scaleType="centerCrop"
       android:visibility="gone" />

   <Button
       android:id="@+id/btn_back"
       android:layout_width="250dp"
       android:layout_height="wrap_content"
       android:text="載入圖片"
       android:textSize="40sp"/>

</LinearLayout>

​ 執行效果如下。

img

宣告:本文轉自【Kotlin】協程

相關文章