一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第27天,点击查看活动详情
本系列专栏 # Kotlin协程专栏
前言
协程概念不再多说,到目前为止,我们知道了协程是非阻塞的,运行在线程上更轻量的Task,通过调试信息可以看得见我们创建的协程。
那本篇文章开始,正式介绍协程用法和原理。
正文
我们知道启动一个线程,只需要new Thread,然后调用其start()方法,便会在操作系统层面创建一个线程来执行其实现的run()方法中代码,但是协程并没有一个类让我们来new或者继承,而是通过几个固定的方法,本篇文章就先介绍使用最多的launch方法。
launch启动协程
话不多说,我们直接看下面代码:
fun main() {
GlobalScope.launch {
println("Hello World! 1")
delay(1000L)
println("Hello World! 2")
}
println("Hello World! 3")
Thread.sleep(2000L)
}
复制代码
这里通过launch和后面的lambda其实就启动了一个协程,而且这个协程已经在运行了,或者可以这样说,lambda代码块即是协程运行的代码块,也可以理解为这段代码就协程。
这比我们创建和运行线程要简单多了,下面是线程创建:
fun main() {
val thread = Thread{
println("hello world")
}
thread.start()
}
复制代码
虽然说麻烦,但是线程有具体的Thread类,所以协程就有点抽象。
(备注:Kotlin是支持高级函数的,而高级函数是可以通过lambda表示,而高级函数的实现其实就是Kotlin内置的FunctionN接口,这个本质是不变的。)
非阻塞
还是上面的代码,我们执行结果如下:
会发现先执行的3,再执行的1和2,也就相当于这里的代码的执行顺序是不按照代码的顺序来执行的,其实这个也非常好理解,因为创建协程是需要时间的,而launch创建出来的协程就像new了一个子线程,互不干扰,所以3先执行是正常的。
而这种也说明launch创建的协程并不会阻塞当前线程的运行。
协程和线程的关系
上面例子代码,我们都会在代码最后调用sleep休眠了2s,有没有考虑是为什么,我们来把那个sleep代码给删了:
fun main() {
GlobalScope.launch() {
println("Hello World! 1 ")
delay(1000L)
println("Hello World! 2 ")
}
println("Hello World! 3 ")
}
复制代码
执行结果如下图所示:
会发现当主线程销毁后,它创建的协程也不会再执行。所以这里sleep是为了主线程不会这么快退出。
射箭模型
从上面launch创建的协程执行结果能看的出来,当这个协程被启动后,主线程并没有获取它执行的结果,而模式就类比于射箭,所以launch创建协程的模式如果构建思维模型的话,可以看成射箭模型。
他们都有的共同点:
-
箭一旦射出去了,目标就无法再改变;协程一旦被launch,那么它当中执行的任务也不会被中途改变。
-
箭如果命中了猎物,猎物不会自动送到我们手上来;launch的协程一旦任务完成了,即便有结果,也没办法直接返回被调用方。
所以launch适合的场景是业务去执行,不需要得到其返回值的情况。
launch源码简析
这里并不真的去看launch的源码实现,而是简单看一下函数参数,简单了解一下,launch函数定义如下:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
复制代码
这里其实很考察Kotlin语法的熟悉程度,我们来简单看一下:
-
launch是一个扩展函数,它的接收者类型是CoroutineScope,这个表示协程的作用域,而Kotlin的扩展方法就表明launch就相当于CoroutineScope的成员方法。
-
第一个参数是CoroutineContext,这个是协程的上下文,默认值就是EmptyCoroutineContext,而这个参数也可以传递Dispatchers,来指定协程运行的线程池。
-
第二个参数是CoroutineStart,表示协程的启动模式,一般是DEFAULT表示立即执行,还有就是LAZY表示懒加载执行。
-
第三个参数是"suspend CoroutineScope.() -> Unit",这是一个函数类型,这表示什么呢 首先表示这个函数是suspend即挂起函数,其次是CoroutineScope类的成员方法或者扩展方法,最后它的参数类型是没有(没有参数),返回值是Unit。
-
返回值是Job,它代表是协程的句柄,并不能返回协程的执行结果,这也就说明了launch启动的协程不能返回结果。
上面只是简单了解,而其中最难理解的就是block的函数类型,我们先来下面代码:
//定义一个扩展函数叫做testFun
suspend fun CoroutineScope.testFun() {
}
复制代码
//testFun变量的类型就是上面block类型变量
val testFun: suspend CoroutineScope.() -> Unit = CoroutineScope::testFun
//这个变量可以作为launch的参数
GlobalScope.launch(EmptyCoroutineContext, CoroutineStart.DEFAULT, testFun)
复制代码
这里的testFun的函数类型匹配成功,不仅仅要求是CoroutineScope的扩展(成员)方法,还必须要有suspend修饰,关于这个suspend关键字的作用,后面我们单独来说,意思就是这是一个挂起函数。
再结合前面知识Kotlin的lambda本质就是FunctionN接口,可以看我之前的文章:
所以我们可以猜想一下,这里的lambda其实就是实现了一个FunctionN接口实例的匿名类对象,至于suspend是啥玩意,我们记住就可以,即launch的block的函数类型是suspend的。
我们来反编译一下代码:
public final class TestCoroutineKt {
public static final void main() {
BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
String var2;
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
var2 = "Hello World! 1";
System.out.println(var2);
this.label = 1;
if (DelayKt.delay(1000L, this) == var3) {
return var3;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
var2 = "Hello World! 2";
System.out.println(var2);
return Unit.INSTANCE;
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
String var0 = "Hello World! 3";
System.out.println(var0);
Thread.sleep(2000L);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
复制代码
会发现上面调用lambda的地方是:
(Function2)(new Function2((Continuation)null){
}
复制代码
会发现里面有Function2接口的实例,这也就验证了我们的猜想,至于为什么会编译为Function2这种2个参数的接口,以及suspend为啥不见了,这些东西我们后面文章再探究。
总结
这里就记住一点,launch启动协程就如射箭,而实际业务中适合启动那种一劳永逸的任务,不需要获取执行结果。