五分钟学习---kotlin 协程
协程是轻量级的线程,但是它不受线程的约束,可以在一个线程中暂停执行,在另外一个线程中恢复执行。
挂起函数:挂起函数使用suspend关键字修饰。(构造函数,属性s/g,委托,匿名函数不能标记为挂起函数)
挂起函数的特点是:可以暂停函数并且在稍后的时间恢复运行。只有挂起函数才能调用挂起函数,这就到导致普通函数需要一个生成一个挂起上下文(协程作用域)来运行挂起函数。suspend函数返回时,它的任务已经处理完成。
Kotlin 编译器将每个挂起函数转换为一个状态机,在每次函数需要挂起时使用回调并进行优化。
协程作用域:协程需要在特定的作用域中运行。协程作用域能够管理协程的生命周期,不会发生协程泄露。协程作用域取消,协程作用域里面的协程也会取消。每个协程作用域都会有一个父级对象。 新的 CoroutineContext = 父级 CoroutineContext + Job(),
系统提供了一些协程作用域的创建方式,比如
- coroutineScope{...} 能够继承父协程作用域。
- runBlocking{...} 运行一个新协程,并阻塞当前线程一直到协程完成。
- GlobalScope 全局的协程作用域,没有绑定job,顶级协程作用域,和应用的生命周期一样长。适用于伴随app生命周期的顶级后台进程,并且需要显示加入@OptIn(DelicateCoroutinesApi::class)这个注解
- supervisorScope {...}能够继承父协程的作用域,内部的取消和异常只能由父协程传递到子协程,子协程的取消和异常无法影响到子协程以外的协程。
- 手动创建 val scope = CoroutineScope(Job() + Dispatchers.Main)
协程的两种启动方式
- launch{...} 启动新协程而不将结果返回给调用方。
GlobalScope.launch {
println("hello coroutine")
}
- async 配合await 可以取得携程的返回结果
var result = GlobalScope.async {
println("hello")
1
}
print("结果是 ${result.await()}")
协程运行状态的判断,如何取消
创建协程之后都会返回一个job类,job:协程的唯一标识,负责管理协程的声明周期。
对于launch{...}返回的job类的isActive、isCompleted、isCancelled三个参数来判断协程的运行状态
cancel()、cancelAandJoin() 方法可以取消协程
async{...}启动的协程运行状态判断方式同上。
fun main() = runBlocking {
val job = launch{
for(i in 1..10){
delay(200L)
println(i)
println("isActive $isActive")
}
}
delay(2000L)
// job.cancel() 取消协程
job.cancelAndJoin() //执行完毕后取消协程
println("isComplete ${job.isCompleted}")
println("isCancelled ${job.isCancelled}")
}
fun main() = runBlocking {
var result = async {
delay(2000L)
1+1
}
println("isActive ${result.isActive}")
println("isComplete ${result.isCompleted}")
result.cancel("取消",null)//通过抛出CancellationException取消协程
var sum = result.await();
println("isComplete ${result.isCompleted}")
println("isCancelled ${result.isCancelled}")
print("sum = $sum")
}
协程调度器:调度器都是扩展自CoroutineDispatcher,具体有一下几个
- Dispatchers.Default 默认调度器,共享后台线程池,适合计算密集型协程。
- Dispatchers.IO 适合Io密集型的阻塞操作。
- Dispatchers.Unconfined 在当前调用帧中开始协程执行,直到第一次暂停,然后协程构建器函数返回。协程稍后将在相应挂起函数使用的任何线程中恢复,而不会将其限制在任何特定线程或池中。 该
Unconfined调度程序通常不应在代码中使用。 - 可以使用newSingleThreadContext和newFixedThreadPoolContext创建私有线程池。
- 可以使用asCoroutineDispatcher扩展函数将任意Executor转换为调度程序。
使用调度器的常见方法
withContext(..)、async(...)、CoroutineScope(...)、launch(...)、runBlocking(...)
协程的切换可以使用withContext()
fun main() = runBlocking {
println(" 0 "+Thread.currentThread().name)
withContext(Dispatchers.Default){
println(" 1 "+Thread.currentThread().name)
}
withContext(Dispatchers.IO){
println(" 2 "+Thread.currentThread().name)
}
val dispather = newSingleThreadContext("my dispathcer")
withContext(dispather){
println(" 3 "+Thread.currentThread().name)
}
dispather.close()
}
打印结果
0 main
1 DefaultDispatcher-worker-1
2 DefaultDispatcher-worker-1
3 my dispathcer
协程的异常处理
普通的异常处理方式:当一个协程由于一个异常而运行失败时,它会传播这个异常并传递给它的父级,接下来,父级会进行下面几步操作:
- 取消它自己的子级;
- 取消它自己;
- 将异常传播并传递给它的父级。异常会到达层级的根部,而且当前 CoroutineScope 所启动的所有协程都会被取消。
使用 SupervisorJob 时,一个子协程的运行失败不会影响到其他子协程。SupervisorJob 不会取消它和它自己的子级,也不会传播异常并传递给它的父级,它会让子协程自己处理异常。
// Scope 控制我的应用中某一层级的协程
val scope = CoroutineScope(Job())
scope.launch {
supervisorScope {
launch { // Child 1 }
launch { // Child 2 }
}
}
只有使用 supervisorScope 或 CoroutineScope(SupervisorJob()) 创建 SupervisorJob 时,它才会像前文描述的一样工作。将 SupervisorJob 作为参数传入一个协程的 Builder 不能带来您想要的效果。
使用 launch 时,异常会在它发生的第一时间被抛出,这样您就可以将抛出异常的代码包裹到 try/catch 中,就像下面的示例这样:
scope.launch {
try {
codeThatCanThrowExceptions()
} catch(e: Exception) { // 处理异常 }
}
当 async 被用作根协程 (CoroutineScope 实例或 supervisorScope 的直接子协程) 时不会自动抛出异常,而是在调用 .await() 时才会抛出异常。
当 async 作为根协程时,为了捕获其中抛出的异常,您可以用 try/catch 包裹调用 .await() 的代码:
supervisorScope {
val deferred = async {
codeThatCanThrowExceptions()
}
try {
deferred.await()
} catch(e: Exception) {
// 处理 async 中抛出的异常 }
}
其他协程所创建的协程中产生的异常总是会被传播,例如:
val scope = CoroutineScope(Job())
scope.launch {
async {
// 如果 async 抛出异常,launch 就会立即抛出异常,而不会调用 .await()
}
}
CoroutineExceptionHandler 是 CoroutineContext 的一个可选元素,它让您可以处理未捕获的异常。
val handler = CoroutineExceptionHandler { context, exception -> println("Caught $exception")}
val scope = CoroutineScope(Job())
scope.launch(handler) {
launch {
throw Exception("Failed coroutine")
}
}
CoroutineExceptionHandler需要正确投递给CoroutineContext对象,否则无法捕获到异常。
Flow
flow 是一种数据源,主要用来发送数据源给消费者。生产者在有新的监听时执行,同时数据流的生命周期会自动处理。
flow操作符的常见用法
fun main() = runBlocking<Unit> {
flow {
for (i in 1..3) {
delay(100)
println("发送 ${Thread.currentThread().name} => $i")
emit(i)
}
}.flowOn(Dispatchers.IO)
.collect { println("接收 ${Thread.currentThread().name} => $it") }
}
运行结果如下:
发送 DefaultDispatcher-worker-1 => 1
接收 main => 1
发送 DefaultDispatcher-worker-1 => 2
接收 main => 2
发送 DefaultDispatcher-worker-1 => 3
接收 main => 3
Flow中间操作符,需要的时候再来使用,
-
map()将一种类型转换成另外一种类型。
-
tarnsform(), 可以发送任意的字符串,任意的数字。
-
take()只取数据流中前几个。
-
buffer()缓冲。
-
conflate() 合并排放,不处理每一个。
-
conbine() 组合。
-
zip() 合并两个流。
-
flatMap() 每一个数据都平铺成一个流。
-
flowOn()切换流的发射上下文。
flow的异常处理:可以使用try catch来处理。onCompletion 不能捕获异常,只能用于判断是否有异常。
catch 操作符可以捕获来自上游的异常
channel的使用与作用
channel是用于进程间通讯的并发原语。如果生产者和消费者的生命周期不同或者彼此完全独立运行,可以使用BroadcastChannel. BroadcastChannel无法感知生命周期,使用结束之后需要手动关闭。
channel 可以在单个协程之间传递单个值。
fun main() = runBlocking<Unit> {
val channel = Channel<Int>()
launch {
for (x in 1..5) channel.send(x * x)
channel.close()
}
repeat(5) { println(channel.receive()) }
println("Done!")
}
结果如下:
1 4 9 16 25 Done!
另外一种实现生产者消费者的方式
fun main() = runBlocking<Unit> {
val mChannel = CoroutineScope(Dispatchers.IO).produce<String> {
for(i in 1..3){
send(" data $i")
println("send $i")
}
close()
}
for(a in mChannel){
println("receive $a")
}
}
channel更多细节后续补充。