Kotlin協程是個什麼東西?

協程是什麼

根據維基百科的定義,協程(Coroutine)是計算機程序的一類組件,推廣了協做式多任務的子程序,容許執行被掛起與被恢復。android

協程(Coroutine)並非一個新詞,馬爾文·康威於1958年發明了術語「coroutine」,並將它用於彙編程序。而在其餘語言,如Go、Python也都有協程的概念,因此它也不是Kotlin獨有的。git

在不一樣的語言層面上,協程的實現方式是不太同樣的,本文介紹的Kotlin協程在本質上,它是一種輕量級的線程github

Kotlin協程是運行在線程中的,這裏的線程能夠是單線程,也能夠是多線程。在單線程使用協程,比不使用協程的耗時並不會少。編程

上面介紹的都是協程的一些概念,以及Kotlin協程的特色。那究竟爲何會有Kotlin協程?它究竟比線程好在哪裏?咱們繼續往下看。promise

Kotlin協程初認識

在Kotlin中,協程就是線程的封裝,它提供了一套標準的API來幫助咱們寫併發任務。回想一下,在Java和Android中,咱們是怎麼寫併發任務的?markdown

Java實現多任務併發

在Java中,咱們可使用線程或者線程池來實現多任務併發:網絡

//線程
new Thread(new Runnable() {
    @Override
    public void run() {
        //耗時的工做
    }
}).start();

//線程池
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.execute(new Runnable() {
    @Override
    public void run() {
        //耗時的工做
    }
});
複製代碼

Android實現多任務併發

在Android中,除了能夠經過Java的方式,建立線程、使用線程池實現多任務併發以外,還能夠AsyncTask等方式來實現多個耗時任務的併發執行:多線程

//AsyncTask
public abstract class AsyncTask<Params, Progress, Result> {
  //線程池中執行,執行耗時任務
  protected abstract Result doInBackground(Params... params);
  //UI線程中執行,後臺任務進度有變化則執行該方法
  protected void onProgressUpdate(Progress... values) {}
  //UI線程執行,耗時任務執行完成後,該方法會被調用,result是任務的返回值
  protected void onPostExecute(Result result) {}
}
複製代碼

不管是Java仍是Android提供的組件,均可以實現多任務併發的執行,可是上面的組件都或多或少存在着問題:併發

  • 耗時任務執行結束後,子線程要將結果傳遞迴主線程,二者之間的通訊不太方便。
  • AsyncTask處理的回調方法比較多,當有多個任務時可能會出現回調嵌套。

協程實現多任務併發

繼續以AsyncTask舉個🌰:異步

AsyncTask<String, Integer, String> task = new AsyncTask<String, Integer, String>() {
    @Override
    protected String doInBackground(String... strings) {
        String userId = getUserId(); //獲取userId
        return userId;
    }

    @Override
    protected void onPostExecute(final String userId) {
        AsyncTask<String, Integer, String> task1 = new AsyncTask<String, Integer, String>() {
            @Override
            protected String doInBackground(String... strings) {
                String name = getUserName(userId); //獲取userName,須要用到userId
                return name;
            }

            @Override
            protected void onPostExecute(String name) {
                textView.setText(name); //設置到TextView控件中
            }
        };
        task1.execute(); //假設task1是一個耗時任務,去獲取userName
    }
};
task.execute(); //假設task是一個耗時任務,去獲取userId
複製代碼

若是是使用協程,上面的例子能夠簡化爲:

GlobalScope.launch(Dispatchers.Main) {
    val userId = getUserId() //耗時任務,這裏會切換到子線程
    val userName = getUserName(userId) //耗時任務,這裏會切換到子線程
    textView.text = userName //設置到TextView控件中,切換到主線程
}

suspend fun getUserId(): String = withContext(Dispatchers.IO) {
    //耗時操做,返回userId
}

suspend fun getUserName(userId: String): String = withContext(Dispatchers.IO) {
    //耗時操做,返回userName
}
複製代碼

上面launch函數的{}的邏輯,就是一個協程。

相比於AsyncTask的寫法,使用kotlin協程有如下好處:

  • 協程將耗時任務和UI更新放在了上下三行處理,消除了AsyncTask的回調嵌套,使用起來更加方便、簡潔。
  • 協程經過掛起與恢復,將耗時任務的結果直接返回給調用方,使得主線程能直接使用子線程的結果,UI更新更加方便。

Kotlin協程的接入與使用

怎麼接入

在模塊的build.gradle中加入如下依賴:

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$1.3.9"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
}
複製代碼

怎麼使用

Kotlin提供了三種方式來建立協程,以下所示:

//方式一
runBlocking { //runBlocking是一個頂級函數
   ...
}

//方式二
GlobalScope.launch { //GlobalScope是一個單例對象,直接使用launch開啓協程
   ...
}

//方式三
val coroutineScope = CoroutineScope(context) //使用CoroutineContext建立CoroutineScope對象,經過launch開啓協程
coroutineScope.launch {
   ...
}
複製代碼
  • 方式一:它是線程阻塞的,它一般被用在單元測試和main函數中,平時的開發中咱們通常不會用到它。
  • 方式二:與方式一相比,它不會阻塞線程,可是它的生命週期和應用是一致的,並且沒法作到取消(後面會講到),因此也不推薦使用。
  • 方式三:經過CoroutineContext來建立一個CoroutineScope對象,經過CoroutineScope.launchCoroutineScope.async能夠開啓協程,經過CoroutineContext也能夠控制協程的生命週期。在開發過程當中,通常推薦使用這種方式開啓協程。

CoroutineContext

上面說到推薦使用CoroutineScope.launch開啓協程,而不論是GlobalScope.launch仍是CoroutineScope.launchlaunch方法的第一個參數就是CoroutineContext,源碼以下:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    ...
}
複製代碼

這裏的context,即CoroutineContext,它的其中一個做用是起到線程切換的功能,即協程體將運行在CoroutineContext表徵的指定的線程中。

Kotlin協程官方定義了幾個值,可供咱們在開發過程當中使用,它們分別是:

  • Dispatchers.Main

協程體將運行在主線程,用於UI的更新等須要在主線程執行的場景,這個你們應該都清楚。

  • Dispatchers.IO

協程體將運行在IO線程,用於IO密集型操做,如網絡請求、文件操做等場景。

  • Dispatchers.Default

協程體將運行在默認的線程,context沒有指定或指定爲Dispatchers.Default,都屬於這種狀況。用於CPU密集型,如涉及到大量計算等場景。要特別注意的是,這裏的默認線程,其實和上面的IO線程共享同一個線程池。

  • Dispatchers.Unconfined

不受限的調度器,在開發中不該該使用它,暫不研究。

看一下下面這個例子:

GlobalScope.launch(Dispatchers.Main) {
    println("Main Dispatcher,  currentThread=${Thread.currentThread().name}")
}
GlobalScope.launch {
    println("Default Dispatcher1, currentThread=${Thread.currentThread().name}")
}
GlobalScope.launch(Dispatchers.IO) {
    println("IO Dispatcher,  currentThread=${Thread.currentThread().name}")
}
GlobalScope.launch(Dispatchers.Default) {
    println("Default Dispatcher2, currentThread=${Thread.currentThread().name}")
}
複製代碼

程序運行結果以下:

image.png

能夠看到,Dispatchers.Main調度器的協程運行在主線程,而無調度器、Dispatchers.IODispatchers.Default調度器的協程運行在同一個線程池。

launch與async

上面提到可使用launch來建立一個協程,可是除了使用launch以外,Kotlin還提供了async來幫助咱們建立協程。二者的區別是:

  • launch:建立一個協程,返回一個Job,可是並不攜帶協程執行後的結果。
  • async:建立一個協程,返回一個Deferred(也是一個Job),並攜帶協程執行後的結果。

async返回的Deferred是一個輕量級的非阻塞future,它表明的是一個將會在稍後提供結果的promise,因此它須要使用await方法來獲得最終結果。拿Kotlin官方的一個例子對async進行說明:

val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // 假設咱們在這裏作了些有用的事
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // 假設咱們在這裏也作了些有用的事
    return 29
}
複製代碼

執行上述代碼,獲得的結果是:

The answer is 42
複製代碼

Kotlin協程的使用場景

線程切換

在介紹《CoroutineContext》一節時,舉的例子中的協程仍是運行在單一線程中。在實際開發過程當中,常見的場景就是線程的切換與恢復,這須要用到withContext方法了。

withContext

咱們繼續以《與線程的對比》這一節的例子來講明:

GlobalScope.launch(Dispatchers.Main) {
    val userId = withContext(Dispatchers.IO) {
        getUserId() //耗時任務,這裏會切換到子線程
    }
    textView.text = userId //設置到TextView控件中,切換到主線程
}
複製代碼

上面是一個典型的網絡請求場景:一開始運行在主線程,而後須要到後臺獲取userId的值(這裏會執行getUserId方法),獲取結束,結果返回後,會切換回主線程,最後更新UI控件。

在獲取userId的時候,調用了getUserId方法,這裏用到了withContext方法,將線程從main切換到了IO線程,當耗時任務執行結束後(即上面的getUserId方法執行完畢),withContext的另一個做用是恢復到切換子線程前的所在線程,對應上面的例子是main線程,因此咱們才能作更新UI控件的操做。

咱們也能夠將withContext的邏輯單獨放到一個方法去管理,以下所示:

GlobalScope.launch(Dispatchers.Main) {
    val userId = getUserIdAsync()
    textView.text = userId //設置到TextView控件中,切換到主線程
}

fun getUserIdAsync() = withContext(Dispatchers.IO) {
    getUserId() //耗時任務,這裏會切換到子線程
}
複製代碼

這樣看上去就像在使用同步調用的方式執行異步邏輯,可是若是按照上面的方式來寫,IDE會報錯的,提示信息是: Suspend function'withContext' should be called only from a coroutine or another suspend funcion

意思是withContext是一個suspend方法,它須要在協程或另一個suspend方法中被調用。

suspend

suspend是Kotlin協程的一個關鍵字,它表示 「掛起」 的意思。因此上面的報錯,只要加上suspend關鍵字就能解決,即:

GlobalScope.launch(Dispatchers.Main) {
    val userId = getUserIdAsync()
    textView.text = userId //設置到TextView控件中,切換到主線程
}

suspend fun getUserIdAsync() = withContext(Dispatchers.IO) {
    getUserId() //耗時任務,這裏會切換到子線程
}
複製代碼

當代碼執行到suspend方法時,當前協程就會被掛起,這裏所說的掛起是非阻塞的,也就是說它不會阻塞當前所在的線程。這就是所謂的「非阻塞式掛起」。

非阻塞式掛起與協程的執行步驟

非阻塞式掛起的一個前提是:涉及的必須是多線程的操做。由於阻塞的概念是針對單線程而言的。當咱們切換了線程,那確定是非阻塞的,由於耗時的操做跑到別的線程了,原來的線程就自由了,該幹嗎幹嗎唄~

若是在主線程中啓動多個協程,那麼協程的執行順序是怎樣的呢?是按照代碼順序執行麼?仍是有別的執行順序?以下代碼所示,假設test方法在主線程中執行,那麼這段代碼應該輸出什麼呢?

//假設test方法運行在主線程
fun test() {
    println("start test fun, thread=${Thread.currentThread().name}")
    //協程A
    GlobalScope.launch(Dispatchers.Main) {
        println("start coroutine1, thread=${Thread.currentThread().name}")
        val userId = getUserIdAsync()
        println("end coroutine1, thread=${Thread.currentThread().name}")
    }
    //協程B
    GlobalScope.launch(Dispatchers.Main) {
        println("start coroutine2, thread=${Thread.currentThread().name}")
        delay(100)
        println("end coroutine2, thread=${Thread.currentThread().name}")
    }
    println("end test fun, thread=${Thread.currentThread().name}")
}

suspend fun getUserIdAsync() = withContext(Dispatchers.IO) {
    println("getUserIdAsync, thread=${Thread.currentThread().name}")
    delay(1000)
    return@withContext "userId from async"
}
複製代碼

在Android中運行上述代碼,執行結果是:

image.png

經過打印的日誌能夠看到,雖然協程的代碼順序在println("end test fun...")以前,可是在執行順序上,協程的啓動仍然在println("end test fun...")以後,結合非阻塞式掛起,下圖展現了協程的執行順序流程:

image.png

參考文檔

一、Kotlin 的協程用力瞥一眼 - 學不會協程?極可能由於你看過的教程都是錯的

二、協程入門指南

三、最全面的kotlin協程

相關文章
相關標籤/搜索