协程笔记
1 协程是如何工作的,和线程的区别
- 进程
- 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
- 线程
- 线程是进程的一个实体,是CPU调度和分派的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
- 协程
- 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
扩展知识:
程序计数器(寄存器):线程特有,用于记录程序执行到哪里。
栈:线程特有,用于保存线程执行过程中的局部变量,操作数栈,动态链接,方法出口等信息。
举个网上的例子:
射雕英雄传看过吧,周伯通教郭靖一手画圆,一手画方,两只手同时操作,左右互搏,这个就是并行。普通人肯定做不到,不信你试试。你不能并行,却可以并发,你先左手画一笔,然后右手画一笔,同一时候只有一只手在操作,来回交替,直到完成两个图案是,这就是并发,协程主要的功能。
相信看到这里很多人就疑惑了,协程和线程完全是两个东西啊,为什么很多人要把他们混在一起呢?之所以混在一起是因为他能解决多线程的一些痛点。比如线程安全,比如回调地狱。
1.1 线程安全
不使用协程
fun main() {
var a = 0
val startTime = System.currentTimeMillis()
repeat(3) {
Thread {
Thread.sleep(1000)
a = doStn(a)
println("result:$a 线程id:${Thread.currentThread().id} 耗时:${System.currentTimeMillis() - startTime}")
}.start()
}
}
fun doStn(input: Int): Int {
return input + 1
}
打印如下
result:2 线程id:24 耗时:1004
result:2 线程id:26 耗时:1004
result:2 线程id:25 耗时:1004
明显存在线程安全问题,我们加锁试试
改成线程安全的
val lock = Any()
fun main() {
var a = 0
val startTime = System.currentTimeMillis()
repeat(3) {
Thread {
synchronized(lock) {
Thread.sleep(1000)
a = doStn(a)
println("result:$a 线程id:${Thread.currentThread().id} 耗时:${System.currentTimeMillis() - startTime}")
}
}.start()
}
}
fun doStn(input: Int): Int {
return input + 1
}
打印如下
result:1 线程id:24 耗时:1002
result:2 线程id:26 耗时:2003
result:3 线程id:25 耗时:3004
使用协程
fun main() = runBlocking {
var a = 0
val startTime = System.currentTimeMillis()
repeat(3) {
launch {
delay(1000)
a = doStn(a)
println("result:$a 线程:${Thread.currentThread().id} 耗时:${System.currentTimeMillis() - startTime}")
}
}
}
fun doStn(input: Int): Int {
return input + 1
}
打印如下:
result:1 线程:1 耗时:1010
result:2 线程:1 耗时:1010
result:3 线程:1 耗时:1011
对比三组数据:
不使用协程:线程不安全
result:2 线程id:24 耗时:1004
result:2 线程id:26 耗时:1004
result:2 线程id:25 耗时:1004不使用协程:线程安全
result:1 线程id:24 耗时:1002
result:2 线程id:26 耗时:2003
result:3 线程id:25 耗时:3004使用协程:
result:1 线程:1 耗时:1010
result:2 线程:1 耗时:1010
result:3 线程:1 耗时:1011
发现如下:
- 不使用协程时候且不加锁时,存在线程安全问题,加了锁之后,耗时明显增加
- 不使用协程时线程id不一样,使用协程后线程id一样
结论如下:
通过代码对比,我们发现,使用协程时,没有切换线程,且线程安全,耗时却不是3000+。之所以造成这个结果的原因如下:
- Thread.sleep(long time)会阻塞线程,且不释放锁。这造成了大量的cpu资源浪费
- 协程不会阻塞线程,而是采用挂起的形式。
- 同一个协程里,不同的launcher{}。并没有切换线程,只是将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈。就比如:左手画圆,右手画方,当左手累了(比如:Thread.sleep()),右手工作。直到圆和方都画完。
1.2 回调地狱
不使用协程
fun main() {
doStn {
println("$it ${Thread.currentThread()}")
}
}
fun doStn(success: (result: Int) -> Unit) {
Thread().run {
Thread.sleep(1000)
success.invoke(1)
}
}
使用协程
fun main() = runBlocking{
val doStn = withContext(Dispatchers.Default) { doStn() }
println("$doStn ${Thread.currentThread()}")
}
suspend fun doStn():Int {
delay(1000)
return 1
}
两个打印信息都是:1 Thread[main,5,main]
可以看到两个打印是一样的,但是对比第一段代码,明显第二个更清爽,维护起来也更舒服。