阅读 157

五分钟学习---kotlin 协程

五分钟学习---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调度程序通常不应在代码中使用
  • 可以使用newSingleThreadContextnewFixedThreadPoolContext创建私有线程池。
  • 可以使用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更多细节后续补充。

文章分类
Android
文章标签