十一、kotlin的协程(三)——热流channel


theme: cyanosis

热流channel —— 管道

管道是什么?

本质上是一个BlockingQueue阻塞队列,只不过多了个可以挂起的函数罢了

为什么要用管道?

  1. 我们可以把 kotlinchannel 当作 BlockingQueue , 但是 channel 使用的是 挂起函数send 代替 阻塞队列的 put, 用挂起函数 receive 代替阻塞队列的 take这样 channel 的优势就有了, 不会阻塞当前线程

  2. channel 是一个允许单向信息传递的数据结构, 从管道的写入端写入数据到管道的读取端读取数据, 这些都是串行的, 它的顺序是不变的

什么是热流?

前面我们知道 冷流 是 flow,需要末端操作(可以看成是开关)才会开启 emit 的函数发射元素过来

而热流呢?

则不需要 什么末端操作(开关),只要 sender 就一定会将元素发送出去,至于 receiver 端是否读取,那就不清楚了

管道怎么用?

管道 hello world

fun main(): Unit = runBlocking {
	val channel = Channel<Int>()
	launch {
		for (x in 1..5) {
			channel.send(x * x)
		}
		channel.close()
	}
	for (item in channel) {
		println(item)
	}
	println("Done")
}

首先,send 发送变量过去就会被挂起, 直到另一方调用 recevie , send 函数才能够重新发送变量过去, 反之, receive 也是, 如果对面没有 send 任何变量, 则会挂起

就把管道当作queue, 把send当作add, 把receive当作get, 然后在管道中加入get阻塞功能就是channel

这里的 send 为什么会阻塞呢? 因为 channel 管道的大小所限,这会在后续说明

其次,热流管道需要close,来表示没有更多的元素发送了,此时程序才会停止,否则会继续阻塞

对比下 flow

flow 中要实现上面这种方式要咋办?

fun main(): Unit = runBlocking {
	val flow = flow {
		for (x in 1..5) {
			emit(x)
		}
	}
	
	flow.collect {
		println(it)
	}
	println("Done")
}

会不会感觉更加的简单,还不需要 close ,也可以不需要 launch

啊? 什么? flow 不需要 launch

可以看 flow 的源码

public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> = SafeFlow(block)

不过在 collect 函数中则需要挂起 suspend

public suspend fun collect(collector: FlowCollector<T>)

jetbrain明显是想要开发者更多的使用 flow 而不是 channel(至少目前是如此)

管道怎么迭代和关闭?

管道迭代
fun main(): Unit = runBlocking {
   val channel = Channel<Int>()
   launch {
      for (x in 1..5) {
         channel.send(x * x)
      }
   }
   // 当管道没有数据的时候, 就会阻塞等待
   for (y in channel) {
      log(y)
   }
   log("Done")
}
管道的关闭及其关闭状态的判断
@ExperimentalCoroutinesApi
fun main(): Unit = runBlocking {
   val channel = Channel<Int>()
   launch {
      for (x in 1..5) {
         if (channel.isClosedForSend) {
            log("send: close send:${channel.isClosedForSend}, receive: ${channel.isClosedForReceive}")
            break
         }
         channel.send(x)
      }
   }
   repeat(5) {
      if (it >= 3) {
         channel.close()
      }
      if (channel.isClosedForReceive) {
         log("receive: close send:${channel.isClosedForSend}, receive: ${channel.isClosedForReceive}")
         return@repeat
      }
      log(channel.receive())
   }
   log("Done")
}

管道的生产者和消费者模型

@OptIn(ExperimentalCoroutinesApi::class)
fun CoroutineScope.produceSquares() = produce {
	for (x in 1..5) send(x * x)
}

fun main(): Unit = runBlocking {
    val channel = produceSquares()
	channel.consumeEach(::println)
	println("Done")
}
管道的结果可以给另一个管道
fun CoroutineScope.produceNumber() = produce<Int> {
   repeat(10) {
      send(it)
   }
}

fun CoroutineScope.square(number: ReceiveChannel<Int>) = produce<Int> {
   for (i in number) {
      send(i * i)
   }
}

fun main(): Unit = runBlocking {
   val number = produceNumber()
   val receiver = square(number)
   repeat(10) {
      log(receiver.receive())
   }
   log("Done")
   coroutineContext.cancelChildren()
}

注意:我们在调用 send 函数后,执行该 send 的协程会被挂起,线程去其他地方运行别的任务去了,所以看起来 while(true) send(xxxx) 好像它会无限发送元素似的,但其实如果管道的大小 buffer 只有一个的话,这里的 send 也只会被执行一次,向管道中发送一个元素,然后管道就满了, send 函数就会被挂起,等到管道中的元素被 receive 读取,send函数才会立即再发一个元素到管道中

管道与无穷质数序列

@ExperimentalCoroutinesApi
fun CoroutineScope.numbersProducer(start: Int) = produce {
   var n = start
   while (true) send(n++)
}

@ExperimentalCoroutinesApi
fun CoroutineScope.filterPrimes(numbers: ReceiveChannel<Int>, prime: Int) =
   produce {
      for (x in numbers) {
         if (x % prime != 0) {
            send(x) // 这里会找到所有满足条件的元素,然后 send 接着协程被挂起,不会无限的读取 numbers 里面的数字
         }
      }
   }

@ExperimentalCoroutinesApi
fun main(): Unit = runBlocking {
   var numbers = numbersProducer(2)
   while (true) {
      val prime = numbers.receive()
      log("$prime")
      numbers = filterPrimes(numbers, prime)
      delay(1000)
   }
}

扇出(读取)

多个协程从一个管道中读取数据

@OptIn(ExperimentalCoroutinesApi::class)
fun CoroutineScope.produceNumbers() = produce {
	var x = 1
	while (true) {
		send(x++)
		delay(100)
	}
}

fun CoroutineScope.launchProcessor(id: Int, channel: ReceiveChannel<Int>) = launch {
	for (i in channel) {
		println("${Thread.currentThread().name}: Processor #$id received $i")
	}
}


fun main(): Unit = runBlocking {
    val produceNumbers = produceNumbers()
	repeat(5) {
		launchProcessor(it, produceNumbers)
	}
	delay(1000)
	produceNumbers.cancel()
}

控制台打印出来不同的协程在做接受

main @coroutine#3: Processor #0 received 1
main @coroutine#3: Processor #0 received 2
main @coroutine#4: Processor #1 received 3
main @coroutine#5: Processor #2 received 4
main @coroutine#6: Processor #3 received 5
main @coroutine#7: Processor #4 received 6
main @coroutine#3: Processor #0 received 7
main @coroutine#4: Processor #1 received 8
main @coroutine#5: Processor #2 received 9

广播channel

一个管道广播数据给多个协程

这种方式和前面扇出方式不同之处在于:

前面是一堆数据,分发给多个协程,每个协程得到的数据不重复

这里是一堆数据复制出多个资源给多个协程,每个协程得到的数据重复

@ObsoleteCoroutinesApi
fun main(): Unit = runBlocking {
    val channel = BroadcastChannel<Int>(Channel.BUFFERED)
    launch {
        List(5) {
            delay(200)
            channel.send(it)
        }
        channel.close()
    }
    List(5) {
        launch {
            val receiveChannel = channel.openSubscription()
            for (i in receiveChannel) {
                log("received: $i")
            }
        }
    }.joinAll()
}

看输出就知道了

main @coroutine#3: received: 0
main @coroutine#4: received: 0
main @coroutine#5: received: 0
main @coroutine#6: received: 0
main @coroutine#7: received: 0
main @coroutine#3: received: 1
main @coroutine#4: received: 1
main @coroutine#5: received: 1
main @coroutine#6: received: 1
main @coroutine#7: received: 1
main @coroutine#3: received: 2
main @coroutine#4: received: 2
main @coroutine#5: received: 2
main @coroutine#6: received: 2
main @coroutine#7: received: 2
main @coroutine#3: received: 3
main @coroutine#4: received: 3
main @coroutine#5: received: 3
main @coroutine#6: received: 3
main @coroutine#7: received: 3
main @coroutine#3: received: 4
main @coroutine#4: received: 4
main @coroutine#5: received: 4
main @coroutine#6: received: 4
main @coroutine#7: received: 4

扇入(发送)

多个协程可以发送到同一个通道

suspend fun sendString(channel: SendChannel<String>, s: String, time: Long) {
	while (true) {
		delay(time)
		channel.send(s)
	}
}

fun main(): Unit = runBlocking {
	val channel = Channel<String>()
	launch { sendString(channel, "foo", 200L) }
	launch { sendString(channel, "BAR!", 500L) }
	repeat(6) {
		println(channel.receive())
	}
	currentCoroutineContext().cancelChildren()
}
foo
foo
BAR!
foo
foo
BAR!

通道缓冲区

我们可以给管道设置一个缓冲区, 如果不设置缓冲区的话, 每次管道只能放一个元素

fun main(): Unit = runBlocking {
	val channel = Channel<Int>(4) // 启动带缓冲的通道
	val sender = launch { // 启动发送者协程
		repeat(10) {
			println("Sending $it") // 在每一个元素发送前打印它们
			channel.send(it) // 将在缓冲区被占满时挂起
		}
	}
	// 没有接收到东西……只是等待……
	delay(1000)
	sender.cancel() // 取消发送者协程
}

然后你会发现, 每次传入管道都是 3 个元素, 然后再读取, 对记得 上面管道缓冲区限定传入的参数是 2 , 但后面却能往管道里传入 3 个元素, 这里需要注意

Sending 0
Sending 1
Sending 2
Sending 3
Sending 4

produce 也是可以设置capacity

@ExperimentalCoroutinesApi
public fun <E> CoroutineScope.produce(
    context: CoroutineContext = EmptyCoroutineContext,
    capacity: Int = 0,
    @BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E> =
    produce(context, capacity, BufferOverflow.SUSPEND, CoroutineStart.DEFAULT, onCompletion = null, block = block)

管道是公平的: 先进先出

发送端和接受端的通道秉承先入先出原则

data class Ball(var hits: Int)

fun main() = runBlocking {
    val table = Channel<Ball>() // 一个共享的 table(桌子)
    launch { player("ping", table) }
    launch { player("pong", table) }
    table.send(Ball(0)) // 乒乓球
    delay(1000) // 延迟 1 秒钟
    coroutineContext.cancelChildren() // 游戏结束,取消它们
}

suspend fun player(name: String, table: Channel<Ball>) {
    for (ball in table) { // 在循环中接收球
        ball.hits++
        println("$name $ball")
        delay(300) // 等待一段时间
        table.send(ball) // 将球发送回去
    }
}
ping Ball(hits=1)
pong Ball(hits=2)
ping Ball(hits=3)
pong Ball(hits=4)

select

select 在很多地方都听过, 比如 linuxselect 机制的多路复用, select 里头如果我没记错是用一个线程监控多个文件句柄(我记得极限是1024个?), 类似于起一条线程不断的轮询每个数组中的某个值, 判断是否有事件需要处理, 不过后续 linux 他们觉得 select 文件句柄和轮询设计不太好, 于是使用上了 pollepoll

golang 中也有 select ,借助管道触发走自己的分支

kotlinselect是什么?

kotlinselectselect是这样的:

从多个onXXX事件中挑选一个可用的且是最快的那一个事件执行

on 开头的函数这里我看成是 JavaScript DOM 中的那种事件函数, 比如 onClick 就是鼠标单击事件

看下面代码:

@ExperimentalCoroutinesApi
fun CoroutineScope.fizz() = produce {
   while (true) {
      delay(300)
      send("Fizz")
   }
}

@ExperimentalCoroutinesApi
fun CoroutineScope.buzz() = produce {
   while (true) {
      delay(500)
      send("Buzz")
   }
}

suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
   select<Unit> {
      fizz.onReceive { s -> log("fizz -> $s") }
      buzz.onReceive { s -> log("buzz -> $s") }
   }
}

@ExperimentalCoroutinesApi
fun main(): Unit = runBlocking {
   val fizz = fizz()
   val buzz = buzz()
   selectFizzBuzz(fizz, buzz)
   // fizz.cancel()
   // buzz.cancel()
   coroutineContext.cancelChildren()
}
fizz -> 'Fizz'
buzz -> 'Buzz!'
fizz -> 'Fizz'
fizz -> 'Fizz'
buzz -> 'Buzz!'
fizz -> 'Fizz'
buzz -> 'Buzz!'

select 中的 on 系列事件函数将会被 select 注册到内部,监控起来

多路 channel 复用

fun main(): Unit = runBlocking {
	val channels = listOf(Channel<String>(), Channel<String>())
	launch {
		delay(100)
		channels[0].send("777")
	}
	launch {
		delay(200)
		channels[1].send("888")
	}
	val res = select<Int> {
		channels.forEach { channel ->
			channel.onReceive { s ->
				s[0] - '0'
			}
		}
	}
	println(res)
	currentCoroutineContext().cancelChildren()
}

最终速度快的先执行, 打印了 7

而这里:

channel.onReceive调用的不是public val onReceive: SelectClause1<E>这个接口的public fun <R> registerSelectClause1(select: SelectInstance<R>, block: suspend (Q) -> R)这个函数

而是 invoke 函数

图片.png

public operator fun <Q> SelectClause1<Q>.invoke(block: suspend (Q) -> R)

如果你不知道 onReceive 函数是什么参数,则可以使用 invoke

图片.png

select 轮询 onSend 事件

@OptIn(ExperimentalCoroutinesApi::class)
private fun CoroutineScope.produceNumbers(side: SendChannel<Int>) = produce<Int> {
	for (num in 1..10) {
		delay(100)
		select<Unit> {
			onSend(num) { // 这里默认将会发送一个 num 数值到管道中,所以下面不需要再次发送
				// it.send(num * 10) // 这段代码如果执行,将会再次往管道中发送一个数字
			}
			side.onSend(num) { // 自动发送 num 到管道中
				// it.send(num * 100)
			}
		}
	}
}

fun main(): Unit = runBlocking {
	val side = Channel<Int>()
	launch {
		side.consumeEach { println("Side channel has $it") }
	}
	produceNumbers(side).consumeEach {
		println("consuming $it")
		delay(250)
	}
	println("Done consuming")
	currentCoroutineContext().cancelChildren()
}
consuming 1
Side channel has 2
Side channel has 3
consuming 4
Side channel has 5
Side channel has 6
consuming 7
Side channel has 8
Side channel has 9
consuming 10
Done consuming

上面这段代码主要写了两个 channel 读写方面的问题

其中主管道为:

produceNumbers(side).consumeEach {
    println("consuming $it")
    delay(250)
}

侧管道为:

launch {
	side.consumeEach { println("Side channel has $it") }
}

其中需要注意:主管道有延迟 250 ms

所以在打印的时候会发现,侧管道打印的比较多一些

如果将delay(100)这段代码删除

效果会更加的明显

@OptIn(ExperimentalCoroutinesApi::class)
private fun CoroutineScope.produceNumbers(side: SendChannel<Int>) = produce<Int> {
   for (num in 1..10) {
      select<Unit> {
         onSend(num) {
            // it.send(num * 10) // 第二次发送
         }
         side.onSend(num) {
           	// it.send(num * 100) // 第二次发送
         }
      }
   }
}
consuming 1
Side channel has 2
Side channel has 3
Side channel has 4
Side channel has 5
Side channel has 6
Side channel has 7
Side channel has 8
Side channel has 9
Side channel has 10
Done consuming

select 轮询各种事件

select 轮询 onAwait

private fun CoroutineScope.asyncString(time: Int) = async {
   delay(time.toLong())
   "Waited for $time ms"
}

private fun CoroutineScope.asyncStringsList(): List<Deferred<String>> {
   val random = Random(3)
   return List(12) {
      asyncString(random.nextInt(1000))
   }
}

在 select 轮询中选一个结果

@Test
fun test01() = runBlocking<Unit> {
   val list = asyncStringsList()
   val result = select<String> {
      list.forEach { deferred ->
         deferred.onAwait.invoke {
            it
         }
      }
   }
   log(result)
}

select每次只获得一个结果,这点需要注意

如果需要 select 获得更多的结果则代码可以改成如下:

fun main(): Unit = runBlocking {
   val list = asyncStringsList()
   list.withIndex().forEach { (index, deferred) ->
      val res = select<String> {
         deferred.onAwait.invoke {
            "Deferred $index produced answer '$it'"
         }
      }
      println(res)
   }
   val countActive = list.count { it.isActive }
   println("$countActive coroutines are still active")
}

总结下 select

select 类似于一个监控器, 不断监控其内部的所有事件, 直到有一个事件被触发, 直接执行, select 就不会再触发其他事件了, 除非再起一个select用于监控

    fun CoroutineScope.asyncString(str: String, time: Long) = async {
      delay(time)
      str
   }
   
   /**
    * 传入一个管道, 从管道中读取数据
    */
   @ExperimentalCoroutinesApi
   private fun CoroutineScope.switchDeferredChannel(input: Channel<Deferred<String>>) = produce {
      
      // 获取管道的第一个数据
      var current = input.receive()
      // 判断管道是否被关闭
      while (isActive) {
         // select 轮询内部事件
         select<Deferred<String>?> {
            // 设置 receiveCatching 事件, 只要管道有该事件就读取出来
            input.onReceiveCatching.invoke {
               it.getOrNull()
            }
            // 间前面事件读取出来的结果再注册一个轮询的 await 事件
            current.onAwait.invoke {
               // 说明我们的 asyncString 函数的 async 已经执行结果, 返回的 deferred 延迟值
               // awit 到我们 asyncString 函数直接的结果了
               send(it)
               // 再从管道中读取下一个 Deferred
               input.receiveCatching().getOrNull()
            }
         }?.let {
            // 把 select 读取出来的结果重新设置为 current 下次循环 再次在 select 中注册 onAwait 事件
            current = it
         } ?: break
      }
   }
   
   @Test
   fun test01() = runBlocking<Unit> {
      val channel = Channel<Deferred<String>>()
      launch {
         for (s in switchDeferredChannel(channel)) {
            log(s)
         }
      }
      channel.send(asyncString("BEGIN", 100))
      // 这里需要等待时间, 否则间之间被 cancel 掉
      delay(1000)
//    channel.close()
      coroutineContext.cancelChildren()
   }

协程异常处理

协程如果被取消就会在挂起点处抛出异常 CancellationException, 正常情况下我们是无法感受到的, 需要专门去捕捉它

挂起点: 是 suspend 函数 + 异步操作 的位置被叫做挂起点

suspend fun main() {
	coroutineScope {
		val job = launch {
			try {
				repeat(100000) {
					delay(1000)
					println("${Thread.currentThread()}: ping")
				}
			}
			catch (e: Exception) {
				e.printStackTrace()
			}
		}
		delay(3000)
		job.cancel()
	}
}
Thread[DefaultDispatcher-worker-1,5,main]: ping
Thread[DefaultDispatcher-worker-1,5,main]: ping
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@5d5390a2

上面的代码挂起点delay 这里,所以异常也将会在 delay 这里被发现和抛出

异常的传播

自动传播异常与向用户暴露异常

协程构造器有两种: 自动传播异常 (launchactor)或向用户暴露异常 (asyncproduce), 如果协程构造器创建于一个根协程(即没有任何协程的子协程是它), 此时自动传播异常方式的异常将被视为未捕获异常, 异常将直接抛出 , 而向用户暴露异常依赖用户来最终消费异常(await或者receive)

自动传播异常的根协程会直接抛出异常

向用户暴露异常根据调用 例如: await 或者 receive 这两个函数调用来发出异常

具体看代码

@OptIn(DelicateCoroutinesApi::class)
fun main(): Unit = runBlocking {
	val job = GlobalScope.launch {
		// 这个异常不需要捕获,直接就把异常抛出了
		throw IndexOutOfBoundsException()
	}
	job.join()
	val deferred = GlobalScope.async {
		// 这里虽然抛出异常,但是不会被执行
		throw ArithmeticException()
	}
//	try {
	// 上面的 throw ArithmeticException 在下面这行代码抛出异常
	// 如果我们没有捕获这个异常的话,将会抛出 ArithmeticException 异常
	deferred.await()
//	} catch (e: ArithmeticException) {
//		e.printStackTrace()
//	}
}

image.png

看抛出异常的行号

第一个异常在: throw IndexOutOfBoundsException() 这里抛出

第二个异常在:deferred.await()这里抛出异常,而不是throw ArithmeticException()这一行代码

CoroutineExceptionHandler

CoroutineExceptionHandler 仅对直接抛出的异常的协程进行捕获(launch这种不是async)

而它也仅对 CoroutineScope的上下文或者根协程中才能捕获

async 无法使用 CroutineExceptionHandler 的方式捕获异常,它只能够在 await 中发现异常

private val handler =  CoroutineExceptionHandler { _, throwable ->
	log("1 handler $throwable")
}

@DelicateCoroutinesApi
fun main(): Unit = runBlocking {
	val job = GlobalScope.launch(handler) {
		throw AssertionError() // 只会拦截此异常
	}
	val deferred = GlobalScope.async(handler) {
		throw ArithmeticException() // async 内的这一行代码 不会捕获异常
	}
	joinAll(job, deferred)
}
fun main() {
	val scope = CoroutineScope(handler)
	scope.launch {
		val job = launch {
			throw AssertionError() // 只会拦截此异常
		}
		val deferred = async {
			throw ArithmeticException() // async 内的这一行代码 不会捕获异常
		}
		joinAll(job, deferred)
	}
	TimeUnit.SECONDS.sleep(2)
}

但是这样不太方便, 因为需要在每个协程中添加上 异常 handler

使用META-INF的配置文件实现全局的异常查看

他不会捕获异常, 仅仅是查看有什么异常, 合适做程序崩溃日志

val scope = CoroutineScope(handler)

GlobalScope.launch(handler)

所以我们还可以这样:

class GlobalCoroutinesException : CoroutineExceptionHandler {
	override val key: CoroutineContext.Key<*>
		get() = CoroutineExceptionHandler
	
	override fun handleException(context: CoroutineContext, exception: Throwable) {
		log("Coroutine exception: $exception")
	}
}

@DelicateCoroutinesApi
fun main(): Unit = runBlocking {
	val job = GlobalScope.launch {
		throw AssertionError()
	}
	val deferred = GlobalScope.async {
		throw ArithmeticException()
	}
	joinAll(job, deferred)
}

然后在 resouces 目录下创建 services 目录, 再在services目录下创建文件 kotlinx.coroutines.CoroutineExceptionHandler

在目录下填入 GlobalCoroutiesException 的包名和类型 coroutines18.exception.GlobalCoroutiesException

就可以运行 main 函数了

不过还是有问题

image.png

取消与异常

协程如果取消, 就会抛出一个 CancellationException , 但是我们无法发现该异常, 需要 catch 去捕获它, 才能发现

而如果子协程的 job 调用了 cancel 它不会取消掉父协程

fun main(): Unit = runBlocking {
    val child = launch {
        try {
            delay(Long.MAX_VALUE)
        } catch (e: Exception) {
            log("我捕获了异常$e")
        }
        finally {
            log("子协程被取消")
        }
    }
    yield()
    log("准备取消子协程: ")
    child.cancelAndJoin()
    yield()
    log("父协程结束")
}

如果协程遇到了 CancellationException以外的异常,它将使用该异常取消它的父协程。

这个行为无法被覆盖,并且用于为结构化的并发提供稳定的协程层级结构。

CoroutineExceptionHandler的实现并不是用于子协程。

在这些示例中,CoroutineExceptionHandler 总是被设置在由 GlobalScope 启动的协程中。将异常处理者设置在 runBlocking 主作用域内启动的协程中是没有意义的,尽管子协程已经设置了异常处理者, 但是主协程也总是会被取消的。

异常聚合: 探讨多个子协程都抛出异常时会怎样?

private val handler = CoroutineExceptionHandler { _, throwable ->
    log("log: $throwable ${throwable.suppressed.get(0)}")
}

@DelicateCoroutinesApi
fun main(): Unit = runBlocking {
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                    delay(Long.MAX_VALUE)
            }
            finally {
                    throw ArithmeticException()
            }
        }
        launch {
            try {
                delay(1000)
            }
            finally {
                throw ConcurrentModificationException()
            }
        }
    }
    job.join()
}

如代码显示, kotlin将会在 异常的 suppressed 属性里放置多个异常

非根协程的异常总是会被逐层传播给根协程

异常的传播还涉及作用域间的问题, 如果我们前面使用的 GlobalScope 创建协程, 意味着该异常在独立的协程作用域中, 而我们使用的 coroutineScope 创建的协程不会这样

coroutineScope 创建的协程中如果子协程遇到异常, 他会往外抛出异常给其父协程, 父协程如果还有父协程, 则会继续传递给父协程的父协程, 直到传递给根协程处理

所以如果任何一个子协程出现了异常(除了CancellationException), 则会将其他协程都停止掉, 其他协程没有异常

这里出现了子协程出错整个协程树都结束的问题, 后面会出现解决的方法 supervisorScope

GlobalScope 创建的协程是独立于其所在的协程的父协程的, 可以说也是根协程, 所以不会出现这种问题

相当于 fork 了一个新的进程,但这里是创建了一个新的协程上下文,异常在协程上下文中传播的,最后都会传播到根协程,不在同一个协程上下文中,异常不可能传播到另一个上下文中

SupervisorJob: 防止子协程异常传播导致整棵协程树失败

fun main(): Unit = runBlocking {
	val scope = CoroutineScope(SupervisorJob())
	val job1 = scope.launch {
		delay(1000)
		throw RuntimeException()
	}
	val job2 = scope.launch {
		repeat(10) {
			delay(150)
			log("协程2 一直再运行")
		}
	}
	joinAll(job1, job2)
}

这样 job1 出现了异常, job2 也可以执行, 直到执行完毕

如果我们需要取消掉所有的协程可以调用 scope.cancel()

supervisorScope

我们也可以像下面这样用

suspend fun main(): Unit = supervisorScope {
	launch {
		repeat(10) {
			delay(200)
			log("子协程活着")
		}
	}
	launch {
		delay(1000)
		throw AssertionError()
	}
}

也能达到相同的效果

但是这种方式存在一个问题

如果作用域内存在作用域的异常, 比如下面这样:

suspend fun main(): Unit = supervisorScope {
	launch {
		repeat(10) {
			delay(200)
			log("子协程活着")
		}
	}
	delay(1000)
	throw AssertionError()
}

协程再也没机会执行 10 次循环了, 它会被 AssertionError 退出整个 supervisorScope 作用域空间

则会继续传递给父协程的父协程, 直到传递给根协程处理**

所以如果任何一个子协程出现了异常(除了CancellationException), 则会将其他协程都停止掉, 其他协程没有异常

这里出现了子协程出错整个协程树都结束的问题, 后面会出现解决的方法 supervisorScope

GlobalScope 创建的协程是独立于其所在的协程的父协程的, 可以说也是根协程, 所以不会出现这种问题

相当于 fork 了一个新的进程,但这里是创建了一个新的协程上下文,异常在协程上下文中传播的,最后都会传播到根协程,不在同一个协程上下文中,异常不可能传播到另一个上下文中

SupervisorJob: 防止子协程异常传播导致整棵协程树失败

fun main(): Unit = runBlocking {
	val scope = CoroutineScope(SupervisorJob())
	val job1 = scope.launch {
		delay(1000)
		throw RuntimeException()
	}
	val job2 = scope.launch {
		repeat(10) {
			delay(150)
			log("协程2 一直再运行")
		}
	}
	joinAll(job1, job2)
}

这样 job1 出现了异常, job2 也可以执行, 直到执行完毕

如果我们需要取消掉所有的协程可以调用 scope.cancel()

supervisorScope

我们也可以像下面这样用

suspend fun main(): Unit = supervisorScope {
	launch {
		repeat(10) {
			delay(200)
			log("子协程活着")
		}
	}
	launch {
		delay(1000)
		throw AssertionError()
	}
}

也能达到相同的效果

但是这种方式存在一个问题

如果作用域内存在作用域的异常, 比如下面这样:

suspend fun main(): Unit = supervisorScope {
	launch {
		repeat(10) {
			delay(200)
			log("子协程活着")
		}
	}
	delay(1000)
	throw AssertionError()
}

协程再也没机会执行 10 次循环了, 它会被 AssertionError 退出整个 supervisorScope 作用域空间

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值