如何爬出 Kotlin 协程死锁的坑?

如何爬出Kotlin协程死锁的坑?

作者:悬衡

一、前言

在 Java 中有一个非常经典的死锁问题, 就是明明自己已经占用了线程池, 却还继续去申请它, 自己等自己, 就死锁了, 如下图和代码:

// 这段代码将死锁到天荒地老final ExecutorService executorService = Executors.newSingleThreadExecutor();executorService.submit(() -> {
  
      Future<?> subTask = executorService.submit(() -> System.out.println("Hello dead lock"));    try {
  
          subTask.get();    } catch (ExecutionException | InterruptedException ignore) { }});

相比别的死锁问题, 这一类问题的坑点在于, 因为线程池的实现问题, jstack 等 jvm 工具无法对其自动诊断, 只能肉眼看出。

在 Kotlin 协程中, 因为底层的线程池申请更加黑盒, 如果不是足够了解, 很容易踩到这类坑。

本文不会再去重复 Kotlin 协程的基本语法, 而是专注于死锁的话题。

下面两段代码你觉得是否有死锁风险?:

  • 第一段代码看起来很恶心, 但是它反而是没有死锁风险

runBlocking(Dispatchers.IO) {
  
      runBlocking {
  
          launch (Dispatchers.IO) {
  
              println("hello coroutine")        }    }}
  • 第二段代码看着 "挺简洁的", 其实是有死锁风险

runBlocking(Dispatchers.IO) {
  
      runBlocking {
  
          launch (Dispatchers.IO) {
  
              println("hello coroutine")        }    }}

只要同一时间有 64 个请求同时进入这个代码块, 就永远不要想出来了, 而且因为协程的线程池都是复用的, 其他协程也别想执行了, 比如下面这段代码就能锁死整个应用:

// 用传统 Java 线程池来模拟 64 个请求val threadPool = Executors.newFixedThreadPool(64)repeat(64) {
  
      threadPool.submit {
  
          runBlocking(Dispatchers.IO) {
  
              println("hello runBlocking $it")            // 在协程环境中本不应该调用 sleep, 这里为了模拟耗时计算和调用,不得已使用            // 正常协程休眠应该用 delay            Thread.sleep(5000)            runBlocking {
  
                  launch (Dispatchers.IO) {
  
                      // 因为死锁, 下面这行永远都打印不出来                    println("hello launch $it")                }            }        }    }}
Thread.sleep(5000)
runBlocking(Dispatchers.IO) {
  
      // 别的协程也执行不了, 下面这行也永远打印不出来    println("hello runBlocking2")}

随便翻翻代码仓库, 就能看到大量存在类似风险的代码, 之前还差点因此发生事故。

本文将会剖析 Kotlin 协程死锁的根本原因, 以及如何彻底地从坑中爬出来。

笔者主要是做服务端的, 文中内容可能更贴近服务端开发场景, 如果移动端场景有所不同, 也欢迎在评论区讨论。

二、runBlocking 线程调度常识

2.1 主线程的独角戏

runBlocking 从表面上理解就是开启一个协程, 并且等待它结束。

Java 的线程思维总让人觉得 runBlocking 会用一个新线程异步执行其中的代码块, 实际上不是这样。runBlocking 在不加参数时, 默认使用当前线程执行:

fun main() {
  
      println("External Thread name: ${Thread.currentThread().name}")    runBlocking {
  
          println("Inner Thread name: ${Thread.currentThread().name}")    }}

输出如下:

External Thread name: mainInner Thread name: main

如果我

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值