Kotlin协程教程(1):启动
协程
协程简单的来说,就是用户态的线程。
emmm,还是不明白对吧,那想象一个这样的场景,如果在一个单核的机器上有两个线程需要执行,因为一次只能执行一个线程里面的代码,那么就会出现线程切换的情况,一会需要执行一下线程A,一会需要执行一下线程B,线程切换会带来一些开销。
假设两个线程,交替执行,如下图所示
Image.png
线程会因为Thread.sleep方法而进入阻塞状态(就是什么也不会执行),这样多浪费资源啊。
能不能将代码块打包成一个个小小的可执行片段,由一个统一的分配器去分配到线程上去执行呢,如果我的代码块里要求sleep一会,那么就去执行别的代码块,等会再来执行我呢。
Image [2].png
协程就是这样一个东西,我们作为使用者不需要再去考虑创建一个新线程去执行一坨代码,也不需要关心线程怎么管理。我们需要关心的是,我要异步的执行一坨代码,待会我要拿到它的结果,我要异步的执行很多坨代码,待会我要按某种顺序,或者某种逻辑得到它们的结果。
总而言之,协程是用户态的线程,它是在用户态实现的一套机制,可以避免线程切换带来的开销,可以高效的利用线程的资源。
从代码上来讲,也可以更漂亮的写各种异步逻辑。
这里想再讲讲一个概念,阻塞与非阻塞是什么意思
阻塞与非阻塞
简单来说,阻塞就是不执行了,非阻塞就是一直在执行。
比如
Thread.wait() // 阻塞了
// 这里执行不到了
但是,如果
while (true) { // 一直在运行,没有阻塞
i++;
}
// 这里也执行不到了
runBlocking:连接阻塞与非阻塞的世界
runBlocking是启动新协程的一种方法。
runBlocking启动一个新的协程,并阻塞它的调用线程,直到里面的代码执行完毕。
举个例子
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,这并没有什么稀奇,所有的代码都在主线程执行,按照顺序来,去掉runBlocking也是一样的嘛。
但是,runBlocking可以指定参数,就可以让runBlocking里面的代码在其他线程执行,但同样可以阻塞外部线程。
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添加了一个参数,Dispatchers.IO,这样里面的代码块就会执行到其他线程了。
来一起看看效果:
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里面的代码,查看这个时候,主线程是什么状态,发现它是进入了WAIT态。
Image [3].png
当给runBlocking指定Dispatchers参数时,就仿佛是使用了join方法。
val t = thread {
for (i in 0..10) {
println("$i ${Thread.currentThread().name}")
Thread.sleep(100)
}
}
t.join()
launch:启动一个协程
launch可以启动一个协程,但不会阻塞调用线程,但是launch必须要在协程作用域中才能调用。
fun main() {
launch {
// no, no, no...
}
runBlocking {
launch {
// is ok
}
}
}
如果要在非协程作用域调用launch,可以使用GlobalScope.launch。
fun main() {
GlobalScope.launch {
// is ok
}
}
同样的launch也是可以传入一个Dispatcher参数来指定它会被分配到什么线程上执行。
此时,大家就会想了,GlobalScope.launch那么方便,是不是只用它就行了?什么时候该用launch,什么时候该用GlobalScope.launch呢?
文档这样说道:GlobalScope.launch会启动一个top-level的协程,它的生命周期将只受到整个应用程序生命周期的限制。
emmmm,那是不是说,普通的launch,它所创建的协程会受到外层的一个作用域的生命周期的影响,而GlobalScope所创建的协程,不收外层的影响。
于是,有了下面的实验
fun main() {
runBlocking(Dispatchers.IO) {
val job = launch { // 外层任务,包裹两个协程
GlobalScope.launch { // 第一个协程
for (i in 0..10) {
println("GlobalScope $i ${Thread.currentThread().name} -----")
delay(100)
}
}
launch { // 第二个协程
for (i in 0..10) {
println("normal launch $i ${Thread.currentThread().name} #####")
delay(100)
}
}
}
delay(300); // 延迟一会,让第二个协程能执行3次左右
job.cancel() // 将外层任务取消了
delay(2000) // 继续延迟,期望看到GlobalScope能继续运行
}
}
看看实验结果
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 -----
如我的预料一样,GlobalScope无法被cancel。
再来看一下文档里面怎么描述的,体会一下:
Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
and are not cancelled prematurely.
接下来,解释一下上面提到的协程作用域的概念。
什么是协程作用域(Coroutine Scope)?
协程作用域是协程运行的作用范围,换句话说,如果这个作用域销毁了,那么里面的协程也随之失效。就好比变量的作用域。
{ // scope start
int a = 100;
} // scope end
println(a); // what is a?
协程作用域也是这样一个作用,可以用来确保里面的协程都有一个作用域的限制。
一个经典的示例就是,比如我们要在Android上使用协程,但是我们不希望Activity销毁了,我的协程还在悄咪咪的干一些事情,我希望它能停止掉。
我们就可以
class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {
// ....
}
这样,里面运行的协程就会随着Activity的销毁而销毁。
launch的返回值:Job
回到launch的话题,launch启动后,会返回一个Job对象,表示这个启动的协程,我们可以方便的通过这个Job对象,取消,等待这个协程。
像这样:
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方法,来等待这个协程执行完毕。这个和Thread的join方法语义一样。
async:启动协程的另一种姿势
launch启动一个协程后,会返回一个Job对象,这个Job对象不含有任何数据,它只是表示启动的协程本身,我们可以通过这个Job对象来对协程进行控制。
假设这样一种场景,我需要同时启动两个协程来搞点事,然后它们分别都会计算出一个Int值,当两个协程都做完了之后,我需要将这两个Int值加在一起并输出。
如果使用launch,我们可能要在外层建立一个变量来记录协程的输出数据了,但是使用async,就可以轻松的解决这个问题!
async的返回值依然是个Job对象,但它可以带上返回值。
上面的小需求可以用下面的代码实现:
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方法来获取返回值,它会等待协程执行完毕,并将返回值吐出来。
这样上面的代码就是两个协程自己吭哧吭哧弄完之后,各自返回了10和20,外层再将它们加起来。
总结
这篇文章,我大概的讲了一下协程的概念和被发明的初衷,以及在kotlin中,启动协程的基本方法,最后再总结一下,方便快速复习。
进程是一个应用程序的资源管理单元,线程是一个执行单元,但当线程这个执行单元需要切换状态,停止,启动,或者大量启动的时候,就会比较消耗资源。我们需要一个更轻巧,更容易被控制的执行单元,这就是协程啦。
本篇介绍了runBlocking方法,它可以在非协程作用域下创建一个协程作用域,它的名字也很好,阻塞的执行,意味着,它会阻塞它的调用线程,直到它内部都执行完毕。
launch和async都可以在协程作用域下启动协程,launch以Job对象的形式返回协程任务本身,可以通过Job来操作协程,async以Deferred对象的形式返回协程任务,可以获取执行流的返回值。
GlobalScope.launch会创建一个顶层的协程,它只受限于整个应用的生命周期,不建议使用。
相关阅读
如果你喜欢这篇文章,欢迎点赞评论打赏
更多干货内容,欢迎关注我的公众号:好奇码农君