Kotlin Coroutines(協程) 完全解析系列:
Kotlin Coroutines(協程) 完全解析(一),協程簡介
Kotlin Coroutines(協程) 完全解析(二),深入理解協程的掛起、恢復與調度
Kotlin Coroutines(協程) 完全解析(三),封裝異步回調、協程間關系及協程的取消
Kotlin Coroutines(協程) 完全解析(四),協程的異常處理
Kotlin Coroutines(協程) 完全解析(五),協程的并發
本文基于 Kotlin v1.3.0-rc-146,Kotlin-Coroutines v1.0.0-RC1
Kotlin 中引入 Coroutine(協程) 的概念,可以幫助編寫異步代碼,目前還是試驗性的。國內詳細介紹協程的資料比較少,所以我打算寫 Kotlin Coroutines(協程) 完全解析的系列文章,希望可以幫助大家更好地理解協程。這是系列文章的第一篇,簡單介紹協程的特點和一些基本概念。協程主要的目的是簡化異步編程,那么先從為什么需要協程來編寫異步代碼開始。
Kotlin Coroutine 終于正式發布了,所以我跟進最新的正式版更新了本文相關內容
1. 為什么需要協程?
異步編程中最為常見的場景是:在后臺線程執行一個復雜任務,下一個任務依賴于上一個任務的執行結果,所以必須等待上一個任務執行完成后才能開始執行。看下面代碼中的三個函數,后兩個函數都依賴于前一個函數的執行結果。
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
}
三個函數中的操作都是耗時操作,因此不能直接在 UI 線程中運行,而且后兩個函數都依賴于前一個函數的執行結果,三個任務不能并行運行,該如何解決這個問題呢?
1.1 回調
常見的做法是使用回調,把之后需要執行的任務封裝為回調。
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)
}
}
}
回調在只有兩個任務的場景是非常簡單實用的,很多網絡請求框架的 onSuccess Listener 就是使用回調,但是在三個以上任務的場景中就會出現多層回調嵌套的問題,而且不方便處理異常。
1.2 Future
Java 8 引入的 CompletableFuture 可以將多個任務串聯起來,可以避免多層嵌套的問題。
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
}
}
上面代碼中使用連接符串聯起三個任務,最后的exceptionally
方法還可以統一處理異常情況,但是只能在 Java 8 以上才能使用。
1.3 Rx 編程
CompletableFuture 的方式有點類似 Rx 系列的鏈式調用,這也是目前大多數推薦的做法。
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 豐富的操作符、簡便的線程調度、異常處理使得大多數人滿意,我也如此,但是還沒有更簡潔易讀的寫法呢?
1.4 協程
下面是使用 Kotlin 協程的代碼:
suspend fun requestToken(): Token { ... } // 掛起函數
suspend fun createPost(token: Token, item: Item): Post { ... } // 掛起函數
fun processPost(post: Post) { ... }
fun postItem(item: Item) {
GlobalScope.launch {
val token = requestToken()
val post = createPost(token, item)
processPost(post)
// 需要異常處理,直接加上 try/catch 語句即可
}
}
使用協程后的代碼非常簡潔,以順序的方式書寫異步代碼,不會阻塞當前 UI 線程,錯誤處理也和平常代碼一樣簡單。
2. 協程是什么
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 協程的定義
先看官方文檔的描述:
協程通過將復雜性放入庫來簡化異步編程。程序的邏輯可以在協程中順序地表達,而底層庫會為我們解決其異步性。該庫可以將用戶代碼的相關部分包裝為回調、訂閱相關事件、在不同線程(甚至不同機器)上調度執行,而代碼則保持如同順序執行一樣簡單。
協程的開發人員 Roman Elizarov 是這樣描述協程的:協程就像非常輕量級的線程。線程是由系統調度的,線程切換或線程阻塞的開銷都比較大。而協程依賴于線程,但是協程掛起時不需要阻塞線程,幾乎是無代價的,協程是由開發者控制的。所以協程也像用戶態的線程,非常輕量級,一個線程中可以創建任意個協程。
總而言之:協程可以簡化異步編程,可以順序地表達程序,協程也提供了一種避免阻塞線程并用更廉價、更可控的操作替代線程阻塞的方法 -- 協程掛起。
3. 協程的基本概念
下面通過上面協程的例子來介紹協程中的一些基本概念:
3.1 掛起函數
suspend fun requestToken(): Token { ... } // 掛起函數
suspend fun createPost(token: Token, item: Item): Post { ... } // 掛起函數
fun processPost(post: Post) { ... }
requestToken
和createPost
函數前面有suspend
修飾符標記,這表示兩個函數都是掛起函數。掛起函數能夠以與普通函數相同的方式獲取參數和返回值,但是調用函數可能掛起協程(如果相關調用的結果已經可用,庫可以決定繼續進行而不掛起),掛起函數掛起協程時,不會阻塞協程所在的線程。掛起函數執行完成后會恢復協程,后面的代碼才會繼續執行。但是掛起函數只能在協程中或其他掛起函數中調用。事實上,要啟動協程,至少要有一個掛起函數,它通常是一個掛起 lambda 表達式。所以suspend
修飾符可以標記普通函數、擴展函數和 lambda 表達式。
掛起函數只能在協程中或其他掛起函數中調用,上面例子中launch
函數就創建了一個協程。
fun postItem(item: Item) {
GlobalScope.launch { // 創建一個新協程
val token = requestToken()
val post = createPost(token, item)
processPost(post)
// 需要異常處理,直接加上 try/catch 語句即可
}
}
launch
函數:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
從上面函數定義中可以看到協程的一些重要的概念:CoroutineContext、CoroutineDispatcher、Job,下面來一一介紹這些概念。
3.1 CoroutineScope 和 CoroutineContext
CoroutineScope,可以理解為協程本身,包含了 CoroutineContext。
CoroutineContext,協程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一個協程的場景。
EmptyCoroutineContext 表示一個空的協程上下文。
3.2 CoroutineDispatcher
CoroutineDispatcher,協程調度器,決定協程所在的線程或線程池。它可以指定協程運行于特定的一個線程、一個線程池或者不指定任何線程(這樣協程就會運行于當前線程)。coroutines-core
中 CoroutineDispatcher 有三種標準實現Dispatchers.Default
、Dispatchers.IO
,Dispatchers.Main
和Dispatchers.Unconfined
,Unconfined 就是不指定線程。
launch
函數定義如果不指定CoroutineDispatcher
或者沒有其他的ContinuationInterceptor
,默認的協程調度器就是Dispatchers.Default
,Default
是一個協程調度器,其指定的線程為共有的線程池,線程數量至少為 2 最大與 CPU 數相同。
3.3 Job & Deferred
Job,任務,封裝了協程中需要執行的代碼邏輯。Job 可以取消并且有簡單生命周期,它有三種狀態:
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 完成時是沒有返回值的,如果需要返回值的話,應該使用 Deferred,它是 Job 的子類public interface Deferred<out T> : Job
。
3.4 Coroutine builders
CoroutineScope.launch
函數屬于協程構建器 Coroutine builders,Kotlin 中還有其他幾種 Builders,負責創建協程。
3.4.1 CoroutineScope.launch {}
CoroutineScope.launch {}
是最常用的 Coroutine builders,不阻塞當前線程,在后臺創建一個新協程,也可以指定協程調度器,例如在 Android 中常用的GlobalScope.launch(Dispatchers.Main) {}
。
fun postItem(item: Item) {
GlobalScope.launch(Dispatchers.Main) { // 在 UI 線程創建一個新協程
val token = requestToken()
val post = createPost(token, item)
processPost(post)
}
}
3.4.2 runBlocking {}
runBlocking {}
是創建一個新的協程同時阻塞當前線程,直到協程結束。這個不應該在協程中使用,主要是為main
函數和測試設計的。
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 {}
不會創建新的協程,在指定協程上運行掛起代碼塊,并掛起該協程直至代碼塊運行完成。
3.4.4 async {}
CoroutineScope.async {}
可以實現與 launch builder 一樣的效果,在后臺創建一個新協程,唯一的區別是它有返回值,因為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()
函數,它也是是個掛起函數,調用時會掛起當前協程直到 async 中代碼執行完并返回某個值。
4. 小結
Kotlin 協程可以極大地簡化異步編程,雖然剛開始接觸的時候學習比較吃力,但是接觸過一段時間相信絕對會愛上它。建議大家也去瀏覽下面推薦的資料,可以更快地了解協程的大概。
推薦閱讀:
Introduction to Coroutines(Roman Elizarov at KotlinConf 2017, slides)