先来看一段很简单的kotlin协程代码:
fun test4() {
GlobalScope.launch {
println("准备执行")
val value = async {
println("执行异步")
1024
}.await()
println("执行完毕:${value}")
}
}
就是中途有段异步执行,代码需要等待这段异步执行完毕才会继续执行下面的操作,现在,让我们用Java实现一遍,用两个Runnable来模拟:
private static final Object COROUTINE_SUSPEND = "suspend";
class MyRunnable implements Runnable {
int label = 0;
@Override
public void run() {
invoke(null);
}
// 这段invoke代码就是上面那段了
public void invoke(Object result) {
Object value = COROUTINE_SUSPEND;
checkException(result);
switch (label) {
case 0:
println("准备执行");
value = async(this);
label = 1;
if (value == COROUTINE_SUSPEND) {
return;
}
break;
case 1:
value = result;
break;
}
int num = (int) value;
println("执行完毕:" + num);
}
};
class AsyncRunnable implements Runnable {
boolean isCompleted;
MyRunnable otherRunnable;
public Object myAsync(MyRunnable runnable) {
otherRunnable = runnable;
executors.submit(this);
return COROUTINE_SUSPEND;
}
@Override
public void run() {
invoke(null);
}
private void invoke(Object result) {
checkException(result);
println("执行异步");
int value = 1024;
isCompleted = true;
otherRunnable.invoke(value);
}
}
private Object async(MyRunnable myRunnable) {
AsyncRunnable asyncRunnable = new AsyncRunnable();
return asyncRunnable.myAsync(myRunnable);
}
其实挺简单的,协程最核心的点就是函数或者一段程序能被挂起,之后能在挂起的位置重新恢复,而这挂起和恢复就是『先返回函数,异步完成后再重新执行』,里面的代码再被分成好几段去执行。
review以下刚才的代码,有两个点非常重要:
(1)调用async函数的时候会把MyRunnable传进去,对应到协程的实现,就是每个suspend函数都有一个隐藏的参数Continuation,这就是典型的CPS变换(continuation Passing Style)。这样的话待异步操作完成后,重新调用刚才参数MyRunnable的invoke方法即可。
(2)依靠label变量记录执行点,这样下次重新invoke的时候就知道上次执行到哪步了,分段的地方就是调用了suspend函数并且返回COROUTINE_SUSPEND标志。
看完之后你再去看kotlin代码编译后的字节码,就是这样实现的。再通俗点理解,你可以把隐藏的Continuation当做是Callback,这就很常见了,Kotlin只是帮你把这些都省去了,让你不需要写那些Callback,把异步代码写得看起来就像同步的代码一样。
那么,接下来就有一个很重要的问题了,其实看那段代码,你用线程池还是Kotlin的协程,其实效率是无差的,那么,除了代码的简洁外,怎么使用Kotlin的协程才能体现出优势?
这里说点我的理解:对于会造成阻塞的IO任务,不管是协程还是线程池,都不可避免地阻塞线程,我们能提高效率的就是把一些原本Java会造成线程阻塞的行为,都用kotlin的协程API替代,这样才能最大化的利用线程的资源。
比如:
- 用delay代替Thread.sleep;
- 用channel代替BlockCollection;
- 用kotlin的yield、join代替线程的yield、join;
- 用协程的Metux代替java的锁机制;
像Thread.sleep这些方法,一旦阻塞到了线程,那么这个线程就没办法继续工作了,在任务量多的时候,就只能重新开新的线程去执行,而我们都知道线程是一个占用资源比较多的机制,而协程的API,都巧妙地避开了这些阻塞方法,用更轻量的操作去实现,比如delay方法,就是起了个定时器再重新去执行,感兴趣可以去研究一下代码。