一、什么是协程?
如果要用简略的语言来描述协程的话,咱们能够将其称为:“相互协作的程序”。
举个简略的比方,同样是 5 行代码,一般的程序,这 5 行代码的运转次序一般会是 1、2、3、4、5;但关于协程来说,代码履行次序或许会是 1、4、5、3、2 这样错乱的。
看下面的代码:
//调用
val sequence = getSequence()
printSequence(sequence)
fun getSequence() = sequence {
Log.d("TAG", "ADD 1")
yield(1)
Log.d("TAG", "ADD 2")
yield(2)
}
fun printSequence(sequence: Sequence<Int>) {
val iterator = sequence.iterator()
val i = iterator.next()
Log.d("TAG", "printSequence:$i")
val j = iterator.next()
Log.d("TAG", "printSequence:$j")
}
//日志
D/TAG: ADD 1
D/TAG: printSequence:1
D/TAG: ADD 2
D/TAG: printSequence:2
咱们看下yield
的描述:
Yields a value to the Iterator being built and suspends until the next value is requested.
翻译成中文便是:生成一个值给正在构建的迭代器,并挂起直到恳求下一个值。
也便是说yield(1)
返回了一个值1
并挂起(Suspend)
了协程函数,等候这个值被从迭代器取出后康复(Resume)
协程函数。所以咱们了解协程不能按一般程序的履行次序来了解。
二、调试Kotlin协程程序
1、打印协程称号
1)方法一:设置 VM 参数
① 在test
部分(其他地方也能够)创立一个kt文件
② 编写一个Main
函数随便写点协程的代码,并点击左边的运转
③ 设置 VM 参数
这时分会发现锤子旁边变成了咱们运转的KT文件
点击进入编辑
在VM options
填入装备-Dkotlinx.coroutines.debug=on
或-ea
,点击确定完成装备。
再次运转就能够看到协程和线程名了
2)方法二:一行代码装备
放在输出协程名前以完成设置,或者装备到Application中也能够。Android工程中打开协程debug模式:
System.setProperty("kotlinx.coroutines.debug", "on" )
2、调试协程程序
后边的版别已经跟调试正常程序没啥差别了。Google在23年2月5号有放出一个新版的协程调试,但IDE肯定要升级到最新版,链接在此。多了一个debug的协程Coroutines
窗口:
三、如何了解 Kotlin 的协程?
看一下下面的比方:
runBlocking { //协程一
Log.d("TAG", "first:${Thread.currentThread().name}")
launch { //协程二
Log.d("TAG", "second:${Thread.currentThread().name}")
delay(100)
}
//线程Sleep了一秒
Thread.sleep(1000)
}
输出日志
2023-02-09 14:51:17.606 18149-18149/com.example.testkotlin D/TAG: first:main @coroutine#1
2023-02-09 14:51:18.609 18149-18149/com.example.testkotlin D/TAG: second:main @coroutine#2
能够看到
- 主线程中有二个协程
@coroutine#1
,@coroutine#2
; - 看输出二条日志的时间,
Sleep
会导致线程中的协程不能履行; - 协程一中开启协程二,尽管协程二开启的代码在协程一中部分代码之前,但协程二的履行在协程一之后,这是由于协程二的调度在协程一之后,不能简略的按代码次序来了解。
用一张图表达便是
协程跟线程的联系,有点像线程与进程的联系,毕竟协程不或许脱离线程运转。所以,协程能够了解为运转在线程傍边的、更加轻量的 Task。
四、协程的轻量
如果测验发动 10 亿个线程,这样的代码运转在大部分的机器上都是会由于内存不足等原因而异常退出的。而如果咱们将代码改用协程来完成的话,结果会怎样呢?
runBlocking {
Log.d("TAG", "runBlocking:${Thread.currentThread().name}")
repeat(1_0000_0000){
launch(Dispatchers.IO) {
Log.d("TAG", "launch:${Thread.currentThread().name}")
}
}
}
结果是能够正常运转的。
别的,协程尽管运转在线程之上,但协程并不会和某个线程绑定,在某些情况下,协程是能够在不同的线程之间切换的。咱们能够来看看下面的代码:
runBlocking(Dispatchers.IO) {
repeat(2) {
launch {
log("before")
delay(100)
log("after")
}
}
delay(2000)
}
fun log(msg: String) {
Log.d("TAG", "${Thread.currentThread().name}:$msg")
}
//输出日志
2023-02-09 17:29:54.072 28862-28912 D/TAG: DefaultDispatcher-worker-3 @coroutine#2:before
2023-02-09 17:29:54.073 28862-28910 D/TAG: DefaultDispatcher-worker-2 @coroutine#3:before
2023-02-09 17:29:54.175 28862-28912 D/TAG: DefaultDispatcher-worker-3 @coroutine#3:after //@coroutine#3切换了线程
2023-02-09 17:29:54.175 28862-28909 D/TAG: DefaultDispatcher-worker-1 @coroutine#2:after //@coroutine#2切换了线程
能够看到@coroutine#2
开端和完毕的线程并不一致,说明协程是能够在不同的线程之间切换的。
五、协程的“非堵塞”
协程对比线程还有一个特色,那便是非堵塞(Non Blocking),而线程则往往是堵塞式的。比方线程的sleep
会导致线程堵塞,而协程的delay
、yield
等只会让协程挂起,等候适宜的时机康复。比方下面的比方:
//线程堵塞
runBlocking(Dispatchers.IO) {
repeat(2) {
log("2")
Thread.sleep(1000L)
log("3")
}
}
//日志
17:56:21.673 30716-30764 D/TAG: DefaultDispatcher-worker-1 @coroutine#1:2
17:56:22.674 30716-30764 D/TAG: DefaultDispatcher-worker-1 @coroutine#1:3
17:56:22.674 30716-30764 D/TAG: DefaultDispatcher-worker-1 @coroutine#1:2
17:56:23.674 30716-30764 D/TAG: DefaultDispatcher-worker-1 @coroutine#1:3
//协程挂起
runBlocking(Dispatchers.IO) {
repeat(2) {
launch { //开启协程
log("0")
delay(1000L) //挂起
log("1")
}
}
}
//日志
18:05:11.148 31883-31940 D/TAG: DefaultDispatcher-worker-3 @coroutine#2:0 //挂起coroutine#2,不会堵塞线程
18:05:11.149 31883-31938 D/TAG: DefaultDispatcher-worker-1 @coroutine#3:0 //挂起coroutine#3
18:05:12.152 31883-31940 D/TAG: DefaultDispatcher-worker-3 @coroutine#2:1 //挂起时间到,康复coroutine#2
18:05:12.152 31883-31938 D/TAG: DefaultDispatcher-worker-1 @coroutine#3:1 //挂起时间到,康复coroutine#3
留意当线程堵塞时会堵塞在线程中运转的协程,协程并不会由于线程堵塞而自行切换线程持续履行任务,比方下面的比方:
runBlocking(Dispatchers.IO) {
launch {
for (i in 0..5) {
if (i == 2) {
Thread.sleep(2000L)
} else {
log("$i")
}
}
}
}
//日志
18:23:16.189 32312-32362 D/TAG: DefaultDispatcher-worker-3 @coroutine#2:0
18:23:16.189 32312-32362 D/TAG: DefaultDispatcher-worker-3 @coroutine#2:1
18:23:18.190 32312-32362 D/TAG: DefaultDispatcher-worker-3 @coroutine#2:3 //过了2秒在同一线程持续打印
18:23:18.190 32312-32362 D/TAG: DefaultDispatcher-worker-3 @coroutine#2:4
18:23:18.190 32312-32362 D/TAG: DefaultDispatcher-worker-3 @coroutine#2:5
咱们用图来了解线程的堵塞与协程的非堵塞。
首先是线程,当某个任务发生了堵塞行为的时分,比方 sleep,当时履行的 Task 就会堵塞后边一切任务的履行。就像下面这张图所展现的相同:
而协程会存在一个相似“调度中心”的东西,它会来完成 Task 任务的履行和调度。除了具有“调度中心”以外,关于每个协程的 Task,还会多出一个相似“抓手”“挂钩”的东西,能够方便咱们对它进行“挂起和康复”。协程任务的整体履行流程,大致会像下图描述的这样:
六、总结
-
广义的协程,能够了解为“相互协作的程序”,也便是“Cooperative-routine”。
-
协程结构,是独立于 Kotlin 规范库的一套结构,它封装了 Java 的线程,对开发者暴露了协程的 API。
-
程序傍边运转的“协程”,能够了解为轻量的线程;
-
一个线程傍边,能够运转成千上万个协程;
-
协程,也能够了解为运转在线程傍边的非堵塞的 Task;
-
协程,经过挂起和康复的能力,完成了“非堵塞”;
-
协程不会与特定的线程绑定,它能够在不同的线程之间灵活切换,而这其实也是经过“挂起
-
和康复”来完成的。
参考了以下内容
什么是“协程思维模型”
Kotlin 协程到底运转在哪个线程里
教程 – 使用 IntelliJ IDEA 调试协程