Kotlin Coroutines(協(xié)程) 完全解析系列:
Kotlin Coroutines(協(xié)程) 完全解析(一)袱瓮,協(xié)程簡(jiǎn)介
Kotlin Coroutines(協(xié)程) 完全解析(二),深入理解協(xié)程的掛起、恢復(fù)與調(diào)度
Kotlin Coroutines(協(xié)程) 完全解析(三)鸠窗,封裝異步回調(diào)、協(xié)程間關(guān)系及協(xié)程的取消
Kotlin Coroutines(協(xié)程) 完全解析(四)胯究,協(xié)程的異常處理
Kotlin Coroutines(協(xié)程) 完全解析(五)稍计,協(xié)程的并發(fā)
本文基于 Kotlin v1.3.0-rc-146,Kotlin-Coroutines v1.0.0-RC1
Kotlin 中引入 Coroutine(協(xié)程) 的概念裕循,可以幫助編寫異步代碼臣嚣,目前還是試驗(yàn)性的。國(guó)內(nèi)詳細(xì)介紹協(xié)程的資料比較少剥哑,所以我打算寫 Kotlin Coroutines(協(xié)程) 完全解析的系列文章硅则,希望可以幫助大家更好地理解協(xié)程。這是系列文章的第一篇星持,簡(jiǎn)單介紹協(xié)程的特點(diǎn)和一些基本概念抢埋。協(xié)程主要的目的是簡(jiǎn)化異步編程,那么先從為什么需要協(xié)程來編寫異步代碼開始督暂。
Kotlin Coroutine 終于正式發(fā)布了揪垄,所以我跟進(jìn)最新的正式版更新了本文相關(guān)內(nèi)容
1. 為什么需要協(xié)程?
異步編程中最為常見的場(chǎng)景是:在后臺(tái)線程執(zhí)行一個(gè)復(fù)雜任務(wù)逻翁,下一個(gè)任務(wù)依賴于上一個(gè)任務(wù)的執(zhí)行結(jié)果饥努,所以必須等待上一個(gè)任務(wù)執(zhí)行完成后才能開始執(zhí)行“嘶兀看下面代碼中的三個(gè)函數(shù)酷愧,后兩個(gè)函數(shù)都依賴于前一個(gè)函數(shù)的執(zhí)行結(jié)果驾诈。
fun requestToken(): Token {
// makes request for a token & waits
return token // returns result when received
}
fun createPost(token: Token, item: Item): Post {
// sends item to the server & waits
return post // returns resulting post
}
fun processPost(post: Post) {
// does some local processing of result
}
三個(gè)函數(shù)中的操作都是耗時(shí)操作,因此不能直接在 UI 線程中運(yùn)行溶浴,而且后兩個(gè)函數(shù)都依賴于前一個(gè)函數(shù)的執(zhí)行結(jié)果乍迄,三個(gè)任務(wù)不能并行運(yùn)行,該如何解決這個(gè)問題呢士败?
1.1 回調(diào)
常見的做法是使用回調(diào)闯两,把之后需要執(zhí)行的任務(wù)封裝為回調(diào)。
fun requestTokenAsync(cb: (Token) -> Unit) { ... }
fun createPostAsync(token: Token, item: Item, cb: (Post) -> Unit) { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
requestTokenAsync { token ->
createPostAsync(token, item) { post ->
processPost(post)
}
}
}
回調(diào)在只有兩個(gè)任務(wù)的場(chǎng)景是非常簡(jiǎn)單實(shí)用的谅将,很多網(wǎng)絡(luò)請(qǐng)求框架的 onSuccess Listener 就是使用回調(diào)漾狼,但是在三個(gè)以上任務(wù)的場(chǎng)景中就會(huì)出現(xiàn)多層回調(diào)嵌套的問題,而且不方便處理異常饥臂。
1.2 Future
Java 8 引入的 CompletableFuture 可以將多個(gè)任務(wù)串聯(lián)起來逊躁,可以避免多層嵌套的問題。
fun requestTokenAsync(): CompletableFuture<Token> { ... }
fun createPostAsync(token: Token, item: Item): CompletableFuture<Post> { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
requestTokenAsync()
.thenCompose { token -> createPostAsync(token, item) }
.thenAccept { post -> processPost(post) }
.exceptionally { e ->
e.printStackTrace()
null
}
}
上面代碼中使用連接符串聯(lián)起三個(gè)任務(wù)隅熙,最后的exceptionally
方法還可以統(tǒng)一處理異常情況稽煤,但是只能在 Java 8 以上才能使用。
1.3 Rx 編程
CompletableFuture 的方式有點(diǎn)類似 Rx 系列的鏈?zhǔn)秸{(diào)用猛们,這也是目前大多數(shù)推薦的做法念脯。
fun requestToken(): Token { ... }
fun createPost(token: Token, item: Item): Post { ... }
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
Single.fromCallable { requestToken() }
.map { token -> createPost(token, item) }
.subscribe(
{ post -> processPost(post) }, // onSuccess
{ e -> e.printStackTrace() } // onError
)
}
RxJava 豐富的操作符、簡(jiǎn)便的線程調(diào)度弯淘、異常處理使得大多數(shù)人滿意,我也如此吉懊,但是還沒有更簡(jiǎn)潔易讀的寫法呢庐橙?
1.4 協(xié)程
下面是使用 Kotlin 協(xié)程的代碼:
suspend fun requestToken(): Token { ... } // 掛起函數(shù)
suspend fun createPost(token: Token, item: Item): Post { ... } // 掛起函數(shù)
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
GlobalScope.launch {
val token = requestToken()
val post = createPost(token, item)
processPost(post)
// 需要異常處理,直接加上 try/catch 語句即可
}
}
使用協(xié)程后的代碼非常簡(jiǎn)潔借嗽,以順序的方式書寫異步代碼态鳖,不會(huì)阻塞當(dāng)前 UI 線程,錯(cuò)誤處理也和平常代碼一樣簡(jiǎn)單恶导。
2. 協(xié)程是什么
2.1 Gradle 引入
dependencies {
// Kotlin
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
// Kotlin Coroutines
compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0-RC1'
}
2.2 協(xié)程的定義
先看官方文檔的描述:
協(xié)程通過將復(fù)雜性放入庫(kù)來簡(jiǎn)化異步編程浆竭。程序的邏輯可以在協(xié)程中順序地表達(dá),而底層庫(kù)會(huì)為我們解決其異步性惨寿。該庫(kù)可以將用戶代碼的相關(guān)部分包裝為回調(diào)邦泄、訂閱相關(guān)事件、在不同線程(甚至不同機(jī)器)上調(diào)度執(zhí)行裂垦,而代碼則保持如同順序執(zhí)行一樣簡(jiǎn)單顺囊。
協(xié)程的開發(fā)人員 Roman Elizarov 是這樣描述協(xié)程的:協(xié)程就像非常輕量級(jí)的線程。線程是由系統(tǒng)調(diào)度的蕉拢,線程切換或線程阻塞的開銷都比較大特碳。而協(xié)程依賴于線程诚亚,但是協(xié)程掛起時(shí)不需要阻塞線程,幾乎是無代價(jià)的午乓,協(xié)程是由開發(fā)者控制的站宗。所以協(xié)程也像用戶態(tài)的線程,非常輕量級(jí)益愈,一個(gè)線程中可以創(chuàng)建任意個(gè)協(xié)程梢灭。
總而言之:協(xié)程可以簡(jiǎn)化異步編程,可以順序地表達(dá)程序腕唧,協(xié)程也提供了一種避免阻塞線程并用更廉價(jià)或辖、更可控的操作替代線程阻塞的方法 -- 協(xié)程掛起。
3. 協(xié)程的基本概念
下面通過上面協(xié)程的例子來介紹協(xié)程中的一些基本概念:
3.1 掛起函數(shù)
suspend fun requestToken(): Token { ... } // 掛起函數(shù)
suspend fun createPost(token: Token, item: Item): Post { ... } // 掛起函數(shù)
fun processPost(post: Post) { ... }
requestToken
和createPost
函數(shù)前面有suspend
修飾符標(biāo)記枣接,這表示兩個(gè)函數(shù)都是掛起函數(shù)颂暇。掛起函數(shù)能夠以與普通函數(shù)相同的方式獲取參數(shù)和返回值,但是調(diào)用函數(shù)可能掛起協(xié)程(如果相關(guān)調(diào)用的結(jié)果已經(jīng)可用但惶,庫(kù)可以決定繼續(xù)進(jìn)行而不掛起)耳鸯,掛起函數(shù)掛起協(xié)程時(shí),不會(huì)阻塞協(xié)程所在的線程膀曾。掛起函數(shù)執(zhí)行完成后會(huì)恢復(fù)協(xié)程县爬,后面的代碼才會(huì)繼續(xù)執(zhí)行。但是掛起函數(shù)只能在協(xié)程中或其他掛起函數(shù)中調(diào)用添谊。事實(shí)上财喳,要啟動(dòng)協(xié)程,至少要有一個(gè)掛起函數(shù)斩狱,它通常是一個(gè)掛起 lambda 表達(dá)式耳高。所以suspend
修飾符可以標(biāo)記普通函數(shù)、擴(kuò)展函數(shù)和 lambda 表達(dá)式所踊。
掛起函數(shù)只能在協(xié)程中或其他掛起函數(shù)中調(diào)用泌枪,上面例子中launch
函數(shù)就創(chuàng)建了一個(gè)協(xié)程。
fun postItem(item: Item) {
GlobalScope.launch { // 創(chuàng)建一個(gè)新協(xié)程
val token = requestToken()
val post = createPost(token, item)
processPost(post)
// 需要異常處理秕岛,直接加上 try/catch 語句即可
}
}
launch
函數(shù):
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
從上面函數(shù)定義中可以看到協(xié)程的一些重要的概念:CoroutineContext碌燕、CoroutineDispatcher、Job继薛,下面來一一介紹這些概念修壕。
3.1 CoroutineScope 和 CoroutineContext
CoroutineScope,可以理解為協(xié)程本身惋增,包含了 CoroutineContext叠殷。
CoroutineContext,協(xié)程上下文诈皿,是一些元素的集合林束,主要包括 Job 和 CoroutineDispatcher 元素像棘,可以代表一個(gè)協(xié)程的場(chǎng)景。
EmptyCoroutineContext 表示一個(gè)空的協(xié)程上下文壶冒。
3.2 CoroutineDispatcher
CoroutineDispatcher缕题,協(xié)程調(diào)度器,決定協(xié)程所在的線程或線程池胖腾。它可以指定協(xié)程運(yùn)行于特定的一個(gè)線程烟零、一個(gè)線程池或者不指定任何線程(這樣協(xié)程就會(huì)運(yùn)行于當(dāng)前線程)。coroutines-core
中 CoroutineDispatcher 有三種標(biāo)準(zhǔn)實(shí)現(xiàn)Dispatchers.Default
咸作、Dispatchers.IO
锨阿,Dispatchers.Main
和Dispatchers.Unconfined
,Unconfined 就是不指定線程记罚。
launch
函數(shù)定義如果不指定CoroutineDispatcher
或者沒有其他的ContinuationInterceptor
墅诡,默認(rèn)的協(xié)程調(diào)度器就是Dispatchers.Default
,Default
是一個(gè)協(xié)程調(diào)度器桐智,其指定的線程為共有的線程池末早,線程數(shù)量至少為 2 最大與 CPU 數(shù)相同。
3.3 Job & Deferred
Job说庭,任務(wù)然磷,封裝了協(xié)程中需要執(zhí)行的代碼邏輯。Job 可以取消并且有簡(jiǎn)單生命周期刊驴,它有三種狀態(tài):
State | [isActive] | [isCompleted] | [isCancelled] |
---|---|---|---|
New (optional initial state) | false |
false |
false |
Active (default initial state) | true |
false |
false |
Completing (optional transient state) | true |
false |
false |
Cancelling (optional transient state) | false |
false |
true |
Cancelled (final state) | false |
true |
true |
Completed (final state) | false |
true |
false |
Job 完成時(shí)是沒有返回值的姿搜,如果需要返回值的話,應(yīng)該使用 Deferred捆憎,它是 Job 的子類public interface Deferred<out T> : Job
痪欲。
3.4 Coroutine builders
CoroutineScope.launch
函數(shù)屬于協(xié)程構(gòu)建器 Coroutine builders,Kotlin 中還有其他幾種 Builders攻礼,負(fù)責(zé)創(chuàng)建協(xié)程。
3.4.1 CoroutineScope.launch {}
CoroutineScope.launch {}
是最常用的 Coroutine builders栗柒,不阻塞當(dāng)前線程礁扮,在后臺(tái)創(chuàng)建一個(gè)新協(xié)程,也可以指定協(xié)程調(diào)度器瞬沦,例如在 Android 中常用的GlobalScope.launch(Dispatchers.Main) {}
太伊。
fun postItem(item: Item) {
GlobalScope.launch(Dispatchers.Main) { // 在 UI 線程創(chuàng)建一個(gè)新協(xié)程
val token = requestToken()
val post = createPost(token, item)
processPost(post)
}
}
3.4.2 runBlocking {}
runBlocking {}
是創(chuàng)建一個(gè)新的協(xié)程同時(shí)阻塞當(dāng)前線程,直到協(xié)程結(jié)束逛钻。這個(gè)不應(yīng)該在協(xié)程中使用僚焦,主要是為main
函數(shù)和測(cè)試設(shè)計(jì)的。
fun main(args: Array<String>) = runBlocking { // start main coroutine
launch { // launch new coroutine in background and continue
delay(1000L)
println("World!")
}
println("Hello,") // main coroutine continues here immediately
delay(2000L) // delaying for 2 seconds to keep JVM alive
}
class MyTest {
@Test
fun testMySuspendingFunction() = runBlocking {
// here we can use suspending functions using any assertion style that we like
}
}
3.4.3 withContext {}
withContext {}
不會(huì)創(chuàng)建新的協(xié)程曙痘,在指定協(xié)程上運(yùn)行掛起代碼塊芳悲,并掛起該協(xié)程直至代碼塊運(yùn)行完成立肘。
3.4.4 async {}
CoroutineScope.async {}
可以實(shí)現(xiàn)與 launch builder 一樣的效果,在后臺(tái)創(chuàng)建一個(gè)新協(xié)程名扛,唯一的區(qū)別是它有返回值谅年,因?yàn)?code>CoroutineScope.async {}返回的是 Deferred 類型。
fun main(args: Array<String>) = runBlocking { // start main coroutine
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() } // start async one coroutine without suspend main coroutine
val two = async { doSomethingUsefulTwo() } // start async two coroutine without suspend main coroutine
println("The answer is ${one.await() + two.await()}") // suspend main coroutine for waiting two async coroutines to finish
}
println("Completed in $time ms")
}
獲取CoroutineScope.async {}
的返回值需要通過await()
函數(shù)肮韧,它也是是個(gè)掛起函數(shù)融蹂,調(diào)用時(shí)會(huì)掛起當(dāng)前協(xié)程直到 async 中代碼執(zhí)行完并返回某個(gè)值。
4. 小結(jié)
Kotlin 協(xié)程可以極大地簡(jiǎn)化異步編程弄企,雖然剛開始接觸的時(shí)候?qū)W習(xí)比較吃力超燃,但是接觸過一段時(shí)間相信絕對(duì)會(huì)愛上它。建議大家也去瀏覽下面推薦的資料拘领,可以更快地了解協(xié)程的大概意乓。
推薦閱讀:
Introduction to Coroutines(Roman Elizarov at KotlinConf 2017, slides)