文章目錄
-
- 前言-協程介紹
- 主流語言對協程的支援
- Android 項目引用
- 建立一個協程
- 取消協程工作
- launch 參數詳解
- 線程排程器 Dispatchers
- withContext
- withContext 的性能
- 綜合演練
- 協程到底是什麼
- 參考資料
協程又稱微線程,從名字可以看出,協程的粒度比線程更小,并且是使用者管理和控制的,多個協程可以運作在一個線程上面。那麼協程出現的背景又是什麼呢,先來看一下目前線程中影響性能的特性:
- 使用鎖機制
- 線程間的上下文切換
- 線程運作和阻塞狀态的切換
以上任意一點都是很消耗cpu性能的。相對來說協程是由程式自身控制,沒有線程切換的開銷,且不需要鎖機制,因為在同一個線程中運作,不存在同時寫變量沖突,在協程中操作共享資源不加鎖,隻需要判斷狀态就行了,是以執行效率比線程高的多。
But , But , But , But , But , But , But , But , But , But , But .......
在 kotlin 語言環境下,協程 僅僅是一個線程架構
, 并沒有什麼高深的東西,這一點會把很多初學者搞暈。
- Lua語言
Lua從5.0版本開始使用協程,通過擴充庫coroutine來實作。
- Python語言
python可以通過 yield/send 的方式實作協程。在python 3.5以後,async/await 成為了更好的替代方案。
- Go語言
Go語言對協程的實作非常強大而簡潔,可以輕松建立成百上千個協程并發執行。
- Java語言
如上文所說,Java語言并沒有對協程的原生支援,但是某些開源架構模拟出了協程的功能,有興趣的小夥伴可以看一看Kilim架構的源碼:https://github.com/kilim/kilim
- C/C++
c/c++需要自己借助ucontext、setjmp、longjmp庫實作,微信開源了c/c++的協程庫libco。
Kotlin
協程庫的
GitHub
位址:https://github.com/Kotlin/kotlinx.coroutines/tree/master/ui/kotlinx-coroutines-android
Gradle
引用
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1"
class MainActivity : AppCompatActivity() {
var tv1: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tv1 = findViewById(R.id.tv1)
//在主線程啟動一個協程
GlobalScope.launch(Dispatchers.Main) {
// launch coroutine in the main thread
for (i in 10 downTo 1) { // countdown from 10 to 1
tv1?.text = "Countdown $i ..." // update text
delay(1000) // wait a second
}
tv1?.text = "Done!"
}
}
}
如果你仔細觀察,你會發現,耗時操作和更新UI 放在一起執行了,納尼?
你會有這樣的疑問,怎麼沒有線程切換,這難道不會卡頓嗎?
答案是不會的,這就是協程的牛逼之處。
還有一點需要注意,上面的代碼中,我們使用
delay(1000)
來做延時操作,
delay
是一個特殊的函數,這裡暫且稱之為挂起函數,它不會阻塞線程,但是會挂起協程,而且它隻能在協程中使用。
再延伸一點,我們能否用
Thread.sleep(1000)
來代替
delay(1000)
, 答案是不能的。我們的協程是在主線程的基礎上建立的,本質上是主線程的小邏輯單元,用
Thread.sleep(1000)
會直接卡死 UI 主線程。
在
java
開發
Android
應用時,我們用子線程執行耗時操作,當然我們也會中斷子線程來達到取消耗時操作的目的。
那麼我們在協程中執行耗時操作的時候,改怎麼取消呢?
GlobalScope.launch
的傳回值是
Job
對象,用
job.cancel()
來取消協程。例子如下:
class MainActivity : AppCompatActivity() {
var tv1: TextView? = null
var mCancelButton: Button? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tv1 = findViewById(R.id.tv1)
mCancelButton = findViewById(R.id.cancel)
//在主線程啟動一個協程
var job = GlobalScope.launch(Dispatchers.Main) {
// launch coroutine in the main thread
for (i in 10 downTo 1) { // countdown from 10 to 1
tv1?.text = "Countdown $i ..." // update text
delay(1000) // wait a second
}
tv1?.text = "Done!"
}
mCancelButton?.setOnClickListener {
job.cancel() //取消協程工作
mCancelButton?.text = "已經取消了"
}
}
}
在上文中,我們已經學會了使用
GlobalScope.launch
建立一個協程,下面我們來看看建立協程所需要的參數,
launch
的參數有三個,依次為
協程上下文
、
協程啟動模式
協程體
:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext, //上下文`
start: CoroutineStart = CoroutineStart.DEFAULT, //啟動模式
block: suspend CoroutineScope.() -> Unit //協程體
): Job
啟動模式
不是一個很複雜的概念,不過我們暫且不管,預設直接允許排程執行。
上下文
可以有很多作用,包括
攜帶參數
,
攔截協程執行
等等,多數情況下我們不需要自己去實作上下文,隻需要使用現成的就好。上下文有一個重要的作用就是線程切換,
Dispatchers.Main
就是一個官方提供的上下文,它可以確定
launch
啟動的協程體運作在
UI
線程當中(除非你自己在
launch
的協程體内部進行線程切換、或者啟動運作在其他有線程切換能力的上下文的協程)。
協程體
就是我們具體執行的代碼
上面我們建立協程的時候,用的是:
GlobalScope.launch(Dispatchers.Main) {
//do some things
}
為了指定coroutines在什麼線程運作,kotlin提供了四種Dispatchers:
Dispatchers | 用途 | 使用場景 |
---|---|---|
Dispatchers.Main | 主線程,和UI互動,執行輕量任務 | 1.call suspend functions。2. call UI functions。 3. Update LiveData |
Dispatchers.IO | 用于網絡請求和檔案通路 | 1. Database。 2.Reading/writing files。3. Networking |
Dispatchers.Default | CPU密集型任務 | 1. Sorting a list。 2.Parsing JSON。 3.DiffUtils |
Dispatchers.Unconfined | 不限制任何制定線程 | 進階排程器,不應該在正常代碼裡使用 |
上面的部分,我們介紹了排程器
Dispatchers
, 那麼具體是怎麼切換線程的,就是用
withContext
函數。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
):
withContext
被
suspend
修飾,說明
suspend
是一個挂起函數。
withContext(Dispatchers.IO)
定義一段代碼塊,這個代碼塊将在排程器
Dispatchers.IO
中運作,方法塊中的任何代碼總是會運作在
IO
排程器中。
舉個例子:
// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}
// Dispatchers.Main
suspend fun get(url: String) =
// Dispatchers.IO
withContext(Dispatchers.IO) {
// Dispatchers.IO
/* perform blocking network IO here */
}
// Dispatchers.Main
}
通過協程,你可以細粒度的控制線程排程,因為
withContext
讓你可以控制任意一行代碼運作在什麼線程上,而不用引入回調來擷取結果。你可将其應用在很小的函數中,例如資料庫操作和網絡請求。是以,比較好的做法是,使用
withContext
確定每個函數在任意排程器上執行都是安全的,包括
Main
,這樣調用者在調用函數時就不需要考慮應該運作在什麼線程上。
對于提供主線程安全性,
withContext
與回調或
RxJava
一樣快。在某些情況下,甚至可以使用協程上下文
withContext
來優化回調。如果一個函數将對資料庫進行10次調用,那麼您可以告訴
Kotlin
在外部的
withContext
中調用一次切換。盡管資料庫會重複調用
withContext
,但是他它将在同一個排程器下,尋找最快路徑。此外,
Dispatchers.Default
和
Dispatchers.IO
之間的協程切換已經過優化,以盡可能避免線程切換。
下面我們來模拟一個真實的網絡請求
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//在子線程啟動一個協程
GlobalScope.launch(Dispatchers.IO) {
//發起一個網絡請求
var result = HttpUtil.get("https://www.baidu.com")
Log.e("zhaoyanjun:22", "${Thread.currentThread().name}")
withContext(Dispatchers.Main) {
//網絡請求成功以後,到主線程更新UI
Log.e("zhaoyanjun:33", "${Thread.currentThread().name}")
}
//再次回到子線程的協程
Log.e("zhaoyanjun:44", "${Thread.currentThread().name}")
}
}
}
日志列印結果:
E/zhaoyanjun:22: DefaultDispatcher-worker-2
E/zhaoyanjun:33: main
E/zhaoyanjun:44: DefaultDispatcher-worker-2
好,堅持讀到這裡的朋友們,你們一定是異步代碼的“受害者”,你們肯定遇到過“回調地獄”,它讓你的代碼可讀性急劇降低;也寫過大量複雜的異步邏輯處理、異常處理,這讓你的代碼重複邏輯增加;因為回調的存在,還得經常處理線程切換,這似乎并不是一件難事,但随着代碼體量的增加,它會讓你抓狂,線上上報的異常因線程使用不當導緻的可不在少數。
而協程可以幫你優雅的處理掉這些。
簡單來說就是,協程是一種非搶占式或者說協作式的計算機程式并發排程的實作,程式可以主動挂起或者恢複執行。這裡還是需要有點兒作業系統的知識的,我們在 Java 虛拟機上所認識到的線程大多數的實作是映射到核心的線程的,也就是說線程當中的代碼邏輯線上程搶到 CPU 的時間片的時候才可以執行,否則就得歇着,當然這對于我們開發者來說是透明的;而經常聽到所謂的協程更輕量的意思是,協程并不會映射成核心線程或者其他這麼重的資源,它的排程在使用者态就可以搞定,任務之間的排程并非搶占式,而是協作式的。
如果大家熟悉 Java 虛拟機的話,就想象一下 Thread 這個類到底是什麼吧,為什麼它的 run 方法會運作在另一個線程當中呢?誰負責執行這段代碼的呢?顯然,咋一看,Thread 其實是一個對象而已,run 方法裡面包含了要執行的代碼——僅此而已。協程也是如此,如果你隻是看标準庫的 API,那麼就太抽象了,但我們開篇交代了,學習協程不要上來去接觸标準庫,kotlinx.coroutines 架構才是我們使用者應該關心的,而這個架構裡面對應于 Thread 的概念就是 Job 了,大家可以看下它的定義:
public interface Job : CoroutineContext.Element {
...
public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean
public fun start(): Boolean
public fun cancel(cause: CancellationException? = null)
public suspend fun join()
...
}
我們再來看看 Thread 的定義:
public class Thread implements Runnable {
...
public final native boolean isAlive();
public synchronized void start() { ... }
@Deprecated
public final void stop() { ... }
public final void join() throws InterruptedException { ... }
...
}
這裡我們非常貼心的省略了一些注釋和不太相關的接口。我們發現,Thread 與 Job 基本上功能一緻,它們都承載了一段代碼邏輯(前者通過 run 方法,後者通過構造協程用到的 Lambda 或者函數),也都包含了這段代碼的運作狀态。
而真正排程時二者才有了本質的差異,具體怎麼排程,我們隻需要知道排程結果就能很好的使用它們了。