注册

如何启动协程

1.launch启动协程


fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello")
}

fun main() {
GlobalScope.launch {
delay(1000L)
println("World!")
}

println("Hello")
Thread.sleep(2000L)
}

//输出结果
//Hello
//World!

上面是两段代码,这两段代码都是通过launch启动了一个协程并且输出结果也是一样的。


第一段代码中的runBlocking是协程的另一种启动方式,这里先看第二段代码中的launch的启动方式;



  • GlobalScope.launch

GlobalScope.launch是一个扩展函数,接收者是CoroutineScope,意思就是协程作用域,这里的launch等价于CoroutineScope的成员方法,如果要调用launch来启动一个协程就必须先拿到CoroutineScope对象。GlobalScope.launch源码如下


public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}

里面有三个参数:



  • context: 意思是上下文,默认是EmptyCoroutineContext,有默认值就可以不传,但是也可以传递Kotlin提供的Dispatchers来指定协程运行在哪一个线程中;
  • start: CoroutineStart代表了协程的启动模式,不传则默认使用DEFAULT(根据上下文立即调度协程执行),除DEFAULT外还有其他类型:





    • LAZY:延迟启动协程,只在需要时才启动。
    • ATOMIC:以一种不可取消的方式,根据其上下文安排执行的协程;
    • UNDISPATCHED:立即执行协程,直到它在当前线程中的第一个挂起点;





  • block: suspend是挂起的意思,CoroutineScope.()是一个扩展函数,Unit是一个函数类似于Java的void,那么suspend CoroutineScope.() -> Unit就可以这么理解了:首先,它是一个挂起函数,然后它还是CoroutineScope类的成员或者扩展函数,参数为空,返回值类型为Unit



  • delay(): delay()方法从字面理解就是延迟的意思,在上面的代码中延迟了1秒再执行World,从源码可以看出来它跟其他方法不一样,多了一个suspend关键字

//      挂起
// ↓
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}

suspend的意思就是挂起,被它修饰的函数就是挂起函数, 这也就意味着delay()方法具有挂起和恢复的能力;



  • Thread.sleep(2000L)

这个是休眠2秒,那么这里为什么要有这个呢?要解答这疑问其实不难,将Thread.sleep(2000L)删除后在运行代码可以发现只打印了Hello然后程序就结束了,World!并没有被打印出来。


为什么? 将上面的代码转换成线程实现如下:


fun main() {
thread(isDaemon = true) {
Thread.sleep(1000L)
println("Hello World!")
}
}

如果不添加isDaemon = true结果输出正常,如果加了那么就没有结果输出。isDaemon的加入后其实是创建了一个【守护线程】,这就意味着主线程结束的时候它会跟着被销毁,所以对于将Thread.sleep删除后导致GlobalScope创建的协程不能正常运行的主要原因就是通过launch创建的协程还没开始执行程序就结束了。那么Thread.sleep(2000L)的作用就是为了不让主线程退出。


另外这里还有一点需要注意:程序的执行过程并不是按照顺序执行的。


fun main() {
GlobalScope.launch { // 1
println("Launch started!") // 2
delay(1000L) // 3
println("World!") // 4
}

println("Hello") // 5
Thread.sleep(2000L) // 6
println("Process end!") // 7
}

/*
输出结果:
Hello
Launch started!
World!
Process end!
*/

上面的代码执行顺序是1、5、6、2、3、4、7,这个其实好理解,首先执行1,然后再执行5,执行6的时候等待2秒,在这个等待过程中协程创建完毕了开始执行2、3、4都可以执行了,当2、3、4执行完毕后等待6执行完毕,最后执行7,程序结束。


2.runBlocking启动协程


fun main() {
runBlocking { // 1
println("launch started!") // 2
delay(1000L) // 3
println("World!") // 4
}

println("Hello") // 5
Thread.sleep(2000L) // 6
println("Process end!") // 7
}

上面这段代码只是将GlobalScope.launch改成了runBlocking,但是执行顺序却完全不一样,它的执行顺讯为代码顺序1~7,这是因为runBlocking是带有阻塞属性的,它会阻塞当前线程的执行。这是它跟launch的最大差异。


runBlockinglanuch的另外一个差异是GlobalScope,从代码中可以看出runBlocking并不需要这个,这点可以从源码中分析


public actual fun <T> runBlocking(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T): T {
...
}

顶层函数:类似于Java中的静态函数,在Java中常用与工具类,例如StringUtils.lastElement();


runBlocking是一个顶层函数,因此可以直接使用它;在它的第二个参数block中有一个返回值类型:T,它刚好跟runBlocking的返回值类型是一样的,因此可以推测出runBlocking是可以有返回值的


fun main() {
val result = test(1)
println("result:$result")
}

fun test(num: Int) = runBlocking {
return@runBlocking num.toString()
}

//输出结果:
//result:1

但是,Kotlin在文档中注明了这个函数不应该从协程中使用。它的设计目的是将常规的阻塞代码与以挂起风格编写的库连接起来,以便在主函数和测试中使用。 因此在正式环境中这种方式最好不用。


3.async启动协程


在 Kotlin 当中,可以使用 async{} 创建协程,并且还能通过它返回的句柄拿到协程的执行结果。


fun main() = runBlocking {
val deferred = async {
1 + 1
}

println("result:${deferred.await()}")
}

//输出结果:
//result:2

上面的代码启动了两个协程,启动方式是runBlockingasync,因为async的调用需要一个作用域,而runBlocking恰好满足这个条件,GlobalScope.launch也可以满足这个条件但是GlobalScope也不建议在生产环境中使用,因为GlobalScope 创建的协程没有父协程,GlobalScope 通常也不与任何生命周期组件绑定。除非手动管理,否则很难满足我们实际开发中的需求。


上面的代码多了一个deferred.await()它就是获取最终结果的关键。


public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}

asynclaunch一样也是一个扩展函数,也有三个参数,和launch的区别在于两点:



  • block的函数类型: launch返回的是Unit类型,async返回的是泛型T
  • 返回值不同: launch返回的是Jobasync返回的是Deffered<T>,而async可以返回执行结果的关键就在这里。

启动协程的三种方式都讲完了,这里存在一个疑问,launchasync都有返回值,为什么async可以获取执行结果,launch却不行?


这主要跟launch的返回值有关,launch的返回值Job代表的是协程的句柄,而句柄并不能返回协程的执行结果。


句柄: 句柄指的是中间媒介,通过这个中间媒介可以控制、操作某样东西。举个例子,door handle 是指门把手,通过门把手可以去控制门,但 door handle 并非 door 本身,只是一个中间媒介。又比如 knife handle 是刀柄,通过刀柄可以使用刀。


协程的三中启动方式区别如下:



  • launch:无法获取执行结果,返回类型Job,不会阻塞;
  • async:可获取执行结果,返回类型Deferred,调用await()会阻塞不调用则不会但也无法获取执行结果;
  • runBlocking:可获取执行结果,阻塞当前线程的执行,多用于Demo、测试,官方推荐只用于连接线程与协程。


作者:无糖可乐爱好者
链接:https://juejin.cn/post/7171981069720223751
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册