協(xié)程
協(xié)程簡單的來說,就是用戶態(tài)的線程。
emmm,還是不明白對吧,那想象一個這樣的場景,如果在一個單核的機器上有兩個線程需要執(zhí)行,因為一次只能執(zhí)行一個線程里面的代碼,那么就會出現(xiàn)線程切換的情況,一會需要執(zhí)行一下線程A,一會需要執(zhí)行一下線程B,線程切換會帶來一些開銷。
假設(shè)兩個線程,交替執(zhí)行,如下圖所示

線程會因為Thread.sleep方法而進入阻塞狀態(tài)(就是什么也不會執(zhí)行),這樣多浪費資源啊。
能不能將代碼塊打包成一個個小小的可執(zhí)行片段,由一個統(tǒng)一的分配器去分配到線程上去執(zhí)行呢,如果我的代碼塊里要求sleep一會,那么就去執(zhí)行別的代碼塊,等會再來執(zhí)行我呢。

協(xié)程就是這樣一個東西,我們作為使用者不需要再去考慮創(chuàng)建一個新線程去執(zhí)行一坨代碼,也不需要關(guān)心線程怎么管理。我們需要關(guān)心的是,我要異步的執(zhí)行一坨代碼,待會我要拿到它的結(jié)果,我要異步的執(zhí)行很多坨代碼,待會我要按某種順序,或者某種邏輯得到它們的結(jié)果。
總而言之,協(xié)程是用戶態(tài)的線程,它是在用戶態(tài)實現(xiàn)的一套機制,可以避免線程切換帶來的開銷,可以高效的利用線程的資源。
從代碼上來講,也可以更漂亮的寫各種異步邏輯。
這里想再講講一個概念,阻塞與非阻塞是什么意思
阻塞與非阻塞
簡單來說,阻塞就是不執(zhí)行了,非阻塞就是一直在執(zhí)行。
比如
Thread.wait() // 阻塞了
// 這里執(zhí)行不到了
但是,如果
while (true) { // 一直在運行,沒有阻塞
i++;
}
// 這里也執(zhí)行不到了
runBlocking:連接阻塞與非阻塞的世界
runBlocking是啟動新協(xié)程的一種方法。
runBlocking啟動一個新的協(xié)程,并阻塞它的調(diào)用線程,直到里面的代碼執(zhí)行完畢。
舉個例子
println("aaaaaaaaa ${Thread.currentThread().name}")
runBlocking {
for (i in 0..10) {
println("$i ${Thread.currentThread().name}")
delay(100)
}
}
println("bbbbbbbbb ${Thread.currentThread().name}")
上面代碼的輸出為:
aaaaaaaaa main
0 main
1 main
2 main
3 main
4 main
5 main
6 main
7 main
8 main
9 main
10 main
bbbbbbbbb main
emmm,這并沒有什么稀奇,所有的代碼都在主線程執(zhí)行,按照順序來,去掉runBlocking也是一樣的嘛。
但是,runBlocking可以指定參數(shù),就可以讓runBlocking里面的代碼在其他線程執(zhí)行,但同樣可以阻塞外部線程。
println("aaaaaaaaa ${Thread.currentThread().name}")
runBlocking(Dispatchers.IO) { // 注意這里
for (i in 0..10) {
println("$i ${Thread.currentThread().name}")
delay(100)
}
}
println("bbbbbbbbb ${Thread.currentThread().name}")
上面的代碼,給runBlocking添加了一個參數(shù),Dispatchers.IO,這樣里面的代碼塊就會執(zhí)行到其他線程了。
來一起看看效果:
aaaaaaaaa main
0 DefaultDispatcher-worker-1
1 DefaultDispatcher-worker-1
2 DefaultDispatcher-worker-1
3 DefaultDispatcher-worker-4
4 DefaultDispatcher-worker-4
5 DefaultDispatcher-worker-6
6 DefaultDispatcher-worker-7
7 DefaultDispatcher-worker-7
8 DefaultDispatcher-worker-9
9 DefaultDispatcher-worker-1
10 DefaultDispatcher-worker-5
bbbbbbbbb main
通過斷點在runBlocking里面的代碼,查看這個時候,主線程是什么狀態(tài),發(fā)現(xiàn)它是進入了WAIT態(tài)。

當給runBlocking指定Dispatchers參數(shù)時,就仿佛是使用了join方法。
val t = thread {
for (i in 0..10) {
println("$i ${Thread.currentThread().name}")
Thread.sleep(100)
}
}
t.join()
launch:啟動一個協(xié)程
launch可以啟動一個協(xié)程,但不會阻塞調(diào)用線程,但是launch必須要在協(xié)程作用域中才能調(diào)用。
fun main() {
launch {
// no, no, no...
}
runBlocking {
launch {
// is ok
}
}
}
如果要在非協(xié)程作用域調(diào)用launch,可以使用GlobalScope.launch。
fun main() {
GlobalScope.launch {
// is ok
}
}
同樣的launch也是可以傳入一個Dispatcher參數(shù)來指定它會被分配到什么線程上執(zhí)行。
此時,大家就會想了,GlobalScope.launch那么方便,是不是只用它就行了?什么時候該用launch,什么時候該用GlobalScope.launch呢?
文檔這樣說道:GlobalScope.launch會啟動一個top-level的協(xié)程,它的生命周期將只受到整個應(yīng)用程序生命周期的限制。
emmmm,那是不是說,普通的launch,它所創(chuàng)建的協(xié)程會受到外層的一個作用域的生命周期的影響,而GlobalScope所創(chuàng)建的協(xié)程,不收外層的影響。
于是,有了下面的實驗
fun main() {
runBlocking(Dispatchers.IO) {
val job = launch { // 外層任務(wù),包裹兩個協(xié)程
GlobalScope.launch { // 第一個協(xié)程
for (i in 0..10) {
println("GlobalScope $i ${Thread.currentThread().name} -----")
delay(100)
}
}
launch { // 第二個協(xié)程
for (i in 0..10) {
println("normal launch $i ${Thread.currentThread().name} #####")
delay(100)
}
}
}
delay(300); // 延遲一會,讓第二個協(xié)程能執(zhí)行3次左右
job.cancel() // 將外層任務(wù)取消了
delay(2000) // 繼續(xù)延遲,期望看到GlobalScope能繼續(xù)運行
}
}
看看實驗結(jié)果
GlobalScope 0 DefaultDispatcher-worker-2 -----
normal launch 0 DefaultDispatcher-worker-5 #####
GlobalScope 1 DefaultDispatcher-worker-5 -----
normal launch 1 DefaultDispatcher-worker-1 #####
GlobalScope 2 DefaultDispatcher-worker-5 -----
normal launch 2 DefaultDispatcher-worker-3 #####
GlobalScope 3 DefaultDispatcher-worker-7 -----
GlobalScope 4 DefaultDispatcher-worker-8 -----
GlobalScope 5 DefaultDispatcher-worker-8 -----
GlobalScope 6 DefaultDispatcher-worker-7 -----
GlobalScope 7 DefaultDispatcher-worker-1 -----
GlobalScope 8 DefaultDispatcher-worker-3 -----
GlobalScope 9 DefaultDispatcher-worker-9 -----
GlobalScope 10 DefaultDispatcher-worker-5 -----
如我的預(yù)料一樣,GlobalScope無法被cancel。
再來看一下文檔里面怎么描述的,體會一下:
Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
and are not cancelled prematurely.
接下來,解釋一下上面提到的協(xié)程作用域的概念。
什么是協(xié)程作用域(Coroutine Scope)?
協(xié)程作用域是協(xié)程運行的作用范圍,換句話說,如果這個作用域銷毀了,那么里面的協(xié)程也隨之失效。就好比變量的作用域。
{ // scope start
int a = 100;
} // scope end
println(a); // what is a?
協(xié)程作用域也是這樣一個作用,可以用來確保里面的協(xié)程都有一個作用域的限制。
一個經(jīng)典的示例就是,比如我們要在Android上使用協(xié)程,但是我們不希望Activity銷毀了,我的協(xié)程還在悄咪咪的干一些事情,我希望它能停止掉。
我們就可以
class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {
// ....
}
這樣,里面運行的協(xié)程就會隨著Activity的銷毀而銷毀。
launch的返回值:Job
回到launch的話題,launch啟動后,會返回一個Job對象,表示這個啟動的協(xié)程,我們可以方便的通過這個Job對象,取消,等待這個協(xié)程。
像這樣:
fun main() {
runBlocking(Dispatchers.IO) {
val job1 = launch {
for (i in 0..10) {
println("normal launch $i ${Thread.currentThread().name} #####")
delay(100)
}
}
val job2 = launch {
for (i in 0..10) {
println("normal launch $i ${Thread.currentThread().name} -----")
delay(100)
}
}
job1.join()
job2.join()
println("all job finished")
}
}
使用job的join方法,來等待這個協(xié)程執(zhí)行完畢。這個和Thread的join方法語義一樣。
async:啟動協(xié)程的另一種姿勢
launch啟動一個協(xié)程后,會返回一個Job對象,這個Job對象不含有任何數(shù)據(jù),它只是表示啟動的協(xié)程本身,我們可以通過這個Job對象來對協(xié)程進行控制。
假設(shè)這樣一種場景,我需要同時啟動兩個協(xié)程來搞點事,然后它們分別都會計算出一個Int值,當兩個協(xié)程都做完了之后,我需要將這兩個Int值加在一起并輸出。
如果使用launch,我們可能要在外層建立一個變量來記錄協(xié)程的輸出數(shù)據(jù)了,但是使用async,就可以輕松的解決這個問題!
async的返回值依然是個Job對象,但它可以帶上返回值。
上面的小需求可以用下面的代碼實現(xiàn):
fun main() {
runBlocking(Dispatchers.IO) {
val job1 = async {
for (i in 0..10) {
println("normal launch $i ${Thread.currentThread().name} #####")
delay(100)
}
10 // 注意這里的返回值
}
val job2 = async {
for (i in 0..10) {
println("normal launch $i ${Thread.currentThread().name} -----")
delay(100)
}
20 // 注意這里的返回值
}
println(job1.await() + job2.await())
println("all job finished")
}
}
這里使用了await方法來獲取返回值,它會等待協(xié)程執(zhí)行完畢,并將返回值吐出來。
這樣上面的代碼就是兩個協(xié)程自己吭哧吭哧弄完之后,各自返回了10和20,外層再將它們加起來。
總結(jié)
這篇文章,我大概的講了一下協(xié)程的概念和被發(fā)明的初衷,以及在kotlin中,啟動協(xié)程的基本方法,最后再總結(jié)一下,方便快速復(fù)習(xí)。
進程是一個應(yīng)用程序的資源管理單元,線程是一個執(zhí)行單元,但當線程這個執(zhí)行單元需要切換狀態(tài),停止,啟動,或者大量啟動的時候,就會比較消耗資源。我們需要一個更輕巧,更容易被控制的執(zhí)行單元,這就是協(xié)程啦。
本篇介紹了runBlocking方法,它可以在非協(xié)程作用域下創(chuàng)建一個協(xié)程作用域,它的名字也很好,阻塞的執(zhí)行,意味著,它會阻塞它的調(diào)用線程,直到它內(nèi)部都執(zhí)行完畢。
launch和async都可以在協(xié)程作用域下啟動協(xié)程,launch以Job對象的形式返回協(xié)程任務(wù)本身,可以通過Job來操作協(xié)程,async以Deferred對象的形式返回協(xié)程任務(wù),可以獲取執(zhí)行流的返回值。
GlobalScope.launch會創(chuàng)建一個頂層的協(xié)程,它只受限于整個應(yīng)用的生命周期,不建議使用。
相關(guān)閱讀
如果你喜歡這篇文章,歡迎點贊評論打賞
更多干貨內(nèi)容,歡迎關(guān)注我的公眾號:好奇碼農(nóng)君
