Kotlin协程作为Kotlin核心的一个组件,上手成本并不高,下面的demo都是我参照官网的例子过了一遍。
其中的Flow大家可以多花点时间,还是挺有意思的。
启动一个协程
fun main() { GlobalScope.launch { println(123) } Thread.sleep(10) }
阻塞方式等待协程执行完再执行后续
GlobalScope.launch { delay(1000) println(123) }.join() println(1231)
delay
特殊的挂起函数,它并不会造成函数阻塞,但是会挂起协程
协程作用域构建器
runBlocking
会阻塞当前线程,直到协程结束。 一般用于测试
runBlocking { launch(Dispatchers.IO) { Log.e("demo", Thread.currentThread().name) delay(10000) println("!23") } }
coroutineScope
只是挂起,会释放底层线程用于其他用途,并不会阻塞线程。所以我们称它为挂起函数
coroutineScope { launch(Dispatchers.IO) { Log.e("demo", Thread.currentThread().name) delay(10000) println("!23") } }
结构化并发
虽然协程使用起来很简单,当我们使用 GlobalScope.launch 时,我们会创建一个顶级协程,但是这样使用也不是我们所推荐的方式,特别是如果我们忘记了对新启动协程的引用,它还是会继续运行。所以在实际应用中,我们更推荐 : 在执行操作所在指定作用域内启动协程,而非随意使用
协程的取消与超时
cancelAndJoin
取消一个协程并等待结束
runBlocking { val startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) { var nextPrintTime = startTime var i = 0 while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU // 每秒打印消息两次 if (System.currentTimeMillis() >= nextPrintTime) { println("job: I'm sleeping ${i++} ...") nextPrintTime += 500L } } } delay(1300L) // 等待一段时间 println("main: I'm tired of waiting!") job.cancelAndJoin() // 取消一个作业并且等待它结束 println("main: Now I can quit.") }
withTimeout
超时后抛出异常
//超时抛出异常 withTimeout(1300L) { delay(1400) }
withTimeoutOrNull
超时后抛出null指针
//超时抛出异常 withTimeoutOrNull(1300L){ delay(1400) }
在finally中释放资源
coroutineScope { val a = launch { try { repeat(1000) { i -> println("日常打印") delay(500) } } finally { println("回收资源") } } delay(1000) //延迟一段时间 println("延迟结束") a.cancelAndJoin() //取消一个作业并等待它结束 }
在finally中重新挂起协程
在我们实际应用中,可能需要在finally重新挂起一个被取消的协程,所以可以将相应的代码包装在
**withContext(NoCancellable)**中
coroutineScope { val a = launch { try { repeat(1000) { i -> println("日常打印") delay(500) } } finally { withContext(NonCancellable){ println("挂起一个被取消的协程") delay(1000) println("挂起取消") } } } delay(1000) println("延迟结束") a.cancelAndJoin() //取消一个作业并等待它结束 }
超时抛出异常
设置超时时间,超过预期时间,抛出异常。可以用于倒计时等
coroutineScope { try { withTimeout(1000){ println("超过2000ms就失败") delay(2000) } }catch (e:TimeoutCancellationException){ println(e.message) println("好的好的,我知道了") } }
超过2000ms就失败 Timed out waiting for 1000 ms 好的好的,我知道了
超时抛出null指针
有些情况,你可能并不想直接抛出异常,则可以让其抛出null指针
coroutineScope { val time = withTimeoutOrNull(1000) { println("超过2000ms就失败") delay(2000) } println(time) //null }
超过2000ms就失败 null
组合挂起函数
默认顺序调用挂起函数
measureTimeMillis
suspend fun main() { val time = measureTimeMillis { playGame() playPP() } println("经过了$time ms") } suspend fun playGame() { delay(1000) println("打豆豆1秒") } suspend fun playPP() { delay(1000) println("打屁屁1秒") }
打豆豆1秒 打屁屁1秒 经过了2031 ms
async
并发执行
在上面的例子中,我们按顺序执行,但我们实际开发中,更多的是希望并行执行,借助于 async 我们就可以实现。
注意
在概念上,async 就类似于 launch。它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于 launch
返回一个 Job 并且不附带任何结果值,,而 async
返回一个 Deferred —— 一个轻量级的非阻塞 future, 这代表了一个将会在稍后提供结果的 promise。你可以使用 .await()
在一个延期的值上得到它的最终结果, 但是 Deferred
也是一个 Job
,所以如果需要的话,你可以取消它。
suspend fun main() { measureTimeMillis { coroutineScope { async { playGame() } async { playPP() } } }.let { println("所费时间+ $it") } } suspend fun playGame() { delay(1000) println("打豆豆1秒") } suspend fun playPP() { delay(1000) println("打屁屁1秒") }
打豆豆1秒 打屁屁1秒 所费时间+ 1084
惰性启动的 async
我们可以通过更改 async 的属性来实现惰性模式,在这个模式下,只有通过 await 或者 async的返回值 job.start,才会启动
注意:如果直接调用await,那么结果将会是顺序执行
suspend fun main() { measureTimeMillis { coroutineScope { val game = async(start = LAZY) { playGame() } val pp = async(start = LAZY) { playPP() } game.start() pp.start() println("启动完成") } }.let(::println) } suspend fun playGame() { delay(1000) println("打豆豆1秒") } suspend fun playPP() { delay(1000) println("打屁屁1秒") }
async 风格的函数
我们可以定义异步风格的函数来异步调用 playGame 和 playPP,并使用 async 协程建造器并带有一个显式的 GlobalScope引用
suspend fun main() { measureTimeMillis { val somethingPlayGame = somethingPlayGame() val somethingPlayPP = somethingPlayPP() runBlocking { somethingPlayGame.await() somethingPlayPP.await() } }.let(::println) } fun somethingPlayGame() = GlobalScope.async { playGame() } fun somethingPlayPP() = GlobalScope.async { playPP() } suspend fun playGame() { delay(1000) println("打豆豆1秒") } suspend fun playPP() { delay(1000) println("打屁屁1秒") }
打屁屁1秒 打豆豆1秒 1085
注意:这样的写法我们并不推荐。如果 val somethingPlayGame = somethingPlayGame() 和 somethingPlayGame.await() 有逻辑错误,程序将抛出异常,但是 somethingPlayPP 依然在后台执行,但是因为前者异常,所有协程都将被关闭,所以 somethingPlayPP 操作也会终止
案例如下:
... suspend fun playGame() { delay(500) throw ArithmeticException("error") println("打豆豆1秒") } ...
Exception in thread "main" java.lang.ArithmeticException: error at com.xiecheng_demo.java.TestKt.playGame(Test.kt:46) ....
使用async正确的结构化并发
按照协程的层次结构传递
suspend fun main() { runBlocking { try { test() } catch (e: ArithmeticException) { println("main-${e.message}") } } } suspend fun test() = coroutineScope { async { playGame() } async { playPP() } } suspend fun playGame() { try { delay(1000) println("打豆豆1秒") } finally { println("我在打豆豆,万一异常了。。。") } } suspend fun playPP() { delay(500) throw ArithmeticException("抛出异常") }
我在打豆豆,万一异常了。。。 main-抛出异常
注意:如果其中一个子协程失败,则第一个 playGame 和等待中的父协程都会被取消
协程上下文和调度器
协程总是运行在以 coroutineContext 为代表的上下文中,协程上下文是各种不同元素的集合,事实上, coroutineContext 就是一个存储协程信息的context
调度器
coroutineContext 包含了dispatchers,我们可以借助其限制协程的工作线程。
分别有如下几种:
- Dispatchers.Default 协程默认线程
- Dispatchers.IO io线程
- Dispatchers.Main 主线程
- Dispatchers.Unconfined 无限制,将直接运行在当前线程
子协程
当一个协程被其他协程在 CoroutineScope 启动时,它将通过 CoroutineScope.CoroutineContext来承袭上下文,并且这个新协程将成为父协程的子作业。当一个父协程被取消时,同时意味着所有的子协程也会取消。
然而,如果此时用 GlobalScope.launch启动子协程,则它与父协程的作用域将无关并且独立运行。
val a = GlobalScope.launch { GlobalScope.launch { println("使用GlobalScope.launch启动") delay(1000) println("GlobalScope.launch-延迟结束") } launch { println("使用 launch 启动") delay(1000) println("launch-延迟结束") } } delay(500) println("取消父launch") a.cancel() delay(1000)
使用GlobalScope.launch启动 使用 launch 启动 取消父launch GlobalScope.launch-延迟结束
join
使用 join 等待所有子协程执行完任务。
suspend fun main() { val a = GlobalScope.launch { GlobalScope.launch { println("使用GlobalScope.launch启动") delay(1000) println("GlobalScope.launch-延迟结束") } launch { println("使用 launch 启动") delay(1000) println("launch-延迟结束") } } a.join() }
在上面main函数中,GlobalScope.launch启动的协程将立即独立执行,如果不使用join,则main可能瞬间执行完成,从而无法看不到效果。使用join方法从而使得 main 所在的协程暂停,直到 GlobalScope.launch 执行完成。
指定协程名
使用 + 操作符来指定
GlobalScope.launch(Dispatchers.Default+CoroutineName("test")){ println(Thread.currentThread().name) }.join()
这里使用了 jvm参数 -Dkotlinx.coroutines.debug
如何配置jvm参数:Android Studio,Intellij同理
协程作用域
在我们了解了上面的概念之后,我们开始将前面学到的结合在一起。定义一个全局的 协程。
class Main4Activity : AppCompatActivity() { private val mainScope = CoroutineScope(Dispatchers.Default) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main4) btn_start.setOnClickListener { mainScope.launch { //一波操作后 launch(Dispatchers.Main) { toast("one") } } Log.e("demo","123") mainScope.launch (Dispatchers.Main) { delay(3000) toastCenter("two") } } btn_cancel.setOnClickListener { onDestrxx() } } fun onDestrxx() { mainScope.cancel() } }
我们在Activity中声明了一个 CoroutineScope 对象,然后创建了一些作用域,这样当我们Activity destory的时候,就可以全部销毁。
这里为了节省代码,仿onDestory 的作用
效果,点击btn1之后,再点击btn2,将只会弹出一个toast,第二个toast将不会弹出
线程局部数据
将一些局部数据传递到协程之间通过 ThreadLoacl即可完成。
suspend fun main() { val threadLocal = ThreadLocal<String>() threadLocal.set("main") GlobalScope.launch(Dispatchers.Default+threadLocal.asContextElement(value = "123")) { println("thread-${Thread.currentThread().name},value=${threadLocal.get()}") println("thread-${Thread.currentThread().name},value=${threadLocal.get()}") }.join() println("thread-${Thread.currentThread().name},value=${threadLocal.get()}") delay(1000) println("thread-${Thread.currentThread().name},value=${threadLocal.get()}") }
thread-DefaultDispatcher-worker-1 @coroutine#1,value=123 thread-DefaultDispatcher-worker-1 @coroutine#1,value=123 thread-main,value=main thread-kotlinx.coroutines.DefaultExecutor,value=null
你可能会疑问**,为什么delay 之后,threadloadl.get为null**?
请注意main函数前面加了一个 suspend,而main函数内部就相当于协程体,当我们直接调用 GlobalScope.launch 时,它直接独立运行,此时内部的 coroutineContext 为我们手动传递的。
而当我们调用了 delay之后,直接挂起协程,此时我们的main函数中的 coroutineContext 即为默认值null,于是get为null