1 前言
相較於 C# 中的協程(詳見 → 【Unity3D】協同程式),Kotlin 中協程更靈活,難度更大。
協程是一種併發設計模式,用於簡化非同步程式設計,它允許以順序化的方式表達非同步操作,避免回撥地獄等問題。使用協程,可以將非同步操作的程式碼像同步程式碼一樣寫,而無需顯式地管理執行緒。
在 Kotlin 中,協程由 kotlinx.coroutines 庫提供支援。它使用 suspend 修飾符來標記掛起函式(即可暫停執行並稍後恢復執行的函式),這使得編寫非同步程式碼更加直觀和簡單。
協程和執行緒具有以下異同點。
1)併發模型
- 執行緒:執行緒是作業系統提供的執行單位,一個程序可以擁有多個執行緒,執行緒之間相對獨立,資料共享需要透過特殊手段(如鎖)保證安全。
- 協程:協程是一種使用者態的輕量級執行緒,由開發者控制其執行與暫停,可以在同一執行緒上併發執行,透過掛起和恢復的方式,實現非阻塞的併發。
2)資源消耗
- 執行緒:每個執行緒都需要分配一定的記憶體和系統資源,執行緒切換時會有一定的開銷。
- 協程:協程是使用者級的,由協程排程器(Coroutine Dispatcher)排程,通常會複用較少的系統資源,因此更輕量級。
3)程式設計模型
- 執行緒:多執行緒程式設計通常以共享狀態和鎖為基礎,編寫併發程式碼較為複雜。
- 協程:協程提供了一種結構化併發程式設計的方式,透過掛起函式的呼叫實現程式碼的暫停和恢復,使得非同步程式設計更易於理解和維護。
4)錯誤處理
- 執行緒:多執行緒程式設計中,錯誤處理相對困難,需要開發者手動處理異常和執行緒間的通訊。
- 協程:協程提供了更加簡單和一致的錯誤處理方式,透過結構化的異常處理機制,可以輕鬆處理協程中的異常。
5)效能
- 執行緒:建立和管理執行緒可能會帶來較大的開銷,尤其是在大量執行緒同時執行時,執行緒切換的開銷也會比較高。
- 協程:協程由於是輕量級的使用者級執行緒,資源消耗較少,因此在大規模併發場景下可能表現更優。
總的來說,協程相比於傳統的執行緒模型,更加靈活、輕量級,並且提供了更加簡單和結構化的併發程式設計方式,使得非同步程式設計更加容易和優雅。
2 協程相關類圖
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 原始碼)
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>
執行效果如下。
宣告:本文轉自【Kotlin】協程。