Kotlin实战指南十三:协程
轉載請標明出處:https://blog.csdn.net/zhaoyanjun6/article/details/95626034
本文出自【趙彥軍的博客】
文章目錄
- 前言-協程介紹
- 主流語言對協程的支持
- 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。
Android 項目引用
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? = nulloverride 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 threadfor (i in 10 downTo 1) { // countdown from 10 to 1tv1?.text = "Countdown $i ..." // update textdelay(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? = nullvar mCancelButton: Button? = nulloverride 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 threadfor (i in 10 downTo 1) { // countdown from 10 to 1tv1?.text = "Countdown $i ..." // update textdelay(1000) // wait a second}tv1?.text = "Done!"}mCancelButton?.setOnClickListener {job.cancel() //取消協程工作mCancelButton?.text = "已經取消了"}} }launch 參數詳解
在上文中,我們已經學會了使用 GlobalScope.launch 創建一個協程,下面我們來看看創建協程所需要的參數,launch 的參數有三個,依次為協程上下文、協程啟動模式、協程體:
public fun CoroutineScope.launch(context: CoroutineContext = EmptyCoroutineContext, //上下文`start: CoroutineStart = CoroutineStart.DEFAULT, //啟動模式block: suspend CoroutineScope.() -> Unit //協程體 ): Job啟動模式不是一個很復雜的概念,不過我們暫且不管,默認直接允許調度執行。
上下文可以有很多作用,包括攜帶參數,攔截協程執行等等,多數情況下我們不需要自己去實現上下文,只需要使用現成的就好。上下文有一個重要的作用就是線程切換,Dispatchers.Main就是一個官方提供的上下文,它可以確保 launch 啟動的協程體運行在UI線程當中(除非你自己在 launch 的協程體內部進行線程切換、或者啟動運行在其他有線程切換能力的上下文的協程)。
協程體就是我們具體執行的代碼
線程調度器 Dispatchers
上面我們創建協程的時候,用的是:
GlobalScope.launch(Dispatchers.Main) {//do some things }為了指定coroutines在什么線程運行,kotlin提供了四種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 | 不限制任何制定線程 | 高級調度器,不應該在常規代碼里使用 |
withContext
上面的部分,我們介紹了調度器 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.Mainval result = get("developer.android.com")// Dispatchers.Mainshow(result) }// Dispatchers.Main suspend fun get(url: String) =// Dispatchers.IOwithContext(Dispatchers.IO) {// Dispatchers.IO/* perform blocking network IO here */}// Dispatchers.Main }通過協程,你可以細粒度的控制線程調度,因為 withContext 讓你可以控制任意一行代碼運行在什么線程上,而不用引入回調來獲取結果。你可將其應用在很小的函數中,例如數據庫操作和網絡請求。所以,比較好的做法是,使用 withContext確保每個函數在任意調度器上執行都是安全的,包括 Main,這樣調用者在調用函數時就不需要考慮應該運行在什么線程上。
withContext 的性能
對于提供主線程安全性,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) {//網絡請求成功以后,到主線程更新UILog.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: Booleanpublic val isCompleted: Booleanpublic val isCancelled: Booleanpublic fun start(): Booleanpublic fun cancel(cause: CancellationException? = null)public suspend fun join()... }我們再來看看 Thread 的定義:
public class Thread implements Runnable {... public final native boolean isAlive();public synchronized void start() { ... }@Deprecatedpublic final void stop() { ... }public final void join() throws InterruptedException { ... }... }這里我們非常貼心的省略了一些注釋和不太相關的接口。我們發現,Thread 與 Job 基本上功能一致,它們都承載了一段代碼邏輯(前者通過 run 方法,后者通過構造協程用到的 Lambda 或者函數),也都包含了這段代碼的運行狀態。
而真正調度時二者才有了本質的差異,具體怎么調度,我們只需要知道調度結果就能很好的使用它們了。
參考資料
Kotlin中文社區 https://www.jianshu.com/p/086a0d681f29
高杰:在Android中使用協程 https://juejin.im/post/5cea3ee0f265da1bca51b841
個人微信號:zhaoyanjun125 , 歡迎關注
總結
以上是生活随笔為你收集整理的Kotlin实战指南十三:协程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Kotlin实战指南十二:data cl
- 下一篇: Kotlin实战指南十四:协程启动模式