【28】Kotlin之协程(二),原理

一、概述

很早前刚了解协程的时候,写了第一篇文章,只是关于协程的简单的一些重要概念,使用方式。这次就想根据自己的成长的了解,说说深入一点的东西。

二、协程 & 线程

很早前,我听朋友将起,协程实现原理是类似线程池,通过线程池管理的方式,避免了线程创建和销毁来带的资源上的开销,毕竟线程的创建和销毁是通过Android系统底层调度实现的。我当时不置可否,甚至已经就认为是对的。

协程既然作为轻量级的并发模型,那绝不仅仅是用户态切换线程这个过程那么简单。

线程
我们知道线程是有自己的栈空间和上下文的,因为应用分配的内存空间是有限的,因此可以管理的线程数量也是有限的。
线程的上下文切换是基于内核实现的,需要依赖操作系统的调度。

协程
而协程不同,它共享线程的栈空间,并且创建和销毁协程的开销非常小,因此可以在线程中创建成百上千个协程,这点很重要。
协程的上下文切换是在用户态完成,保存的数据信息也非常的少。

字节码分析
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

//编译字节码
public final class MainKt$main$1 extends ContinuationImpl {
    int label;

    @Override
    public Object invokeSuspend(Object result) {
        switch (label) {
            case 0:
                label = 1;
                if (delay(1000L, this) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;
                break;
            case 1:
                println("World!");
                return Unit.INSTANCE;
        }
        return Unit.INSTANCE;
    }
}

可以看到,协程在转译成字节码文件的时候,继承了一个ContinuationImpl的类, 并实现它的方法。当执行挂起函数delay的时候,会置label为0,并且返回COROUTINE_SUSPENDED挂起协程,将控制权返回调用者。当挂起结束,会置label为1,并从挂起点恢复执行。

ContinuationImpl是一个状态机类,而label就是管理的状态变量,用于跟踪协程的当前状态,每个挂起点都会对应一个状态。挂起函数的调用会被转换成对Continuation的调用。

挂起函数

这里的挂起函数指的是两个东西,一个是具体的挂起函数,比如delay,明确当前函数会被挂起,并在特定下恢复。另一个是suspend关键字。

suspend用来修饰一个函数是挂起函数,但他只是标记的作用,表示当前函数可以被挂起,并且提供协程域,可以进行协程相关的操作。但是即使是修饰,函数也可能不执行任何挂起操作。

suspend fun fetchData(): String {
    // 这里可以调用其他挂起函数
    return "data"
}

suspend fun fetchData(): String {
    delay(1000) // 挂起协程 1 秒钟
    return "data"
}
并发模型

协程可以让异步操作像同步一样执行。这就是协程挂起的特性,和阻塞不同,它不会阻塞所在线程的执行。

Thread thread = new Thread(() -> {
            try {
                System.out.println("Thread started");
                Thread.sleep(1000); // 阻塞操作
                System.out.println("Thread resumed");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();

我们可以看到,sleep的阻塞函数,让线程进入阻塞状态,直到1秒后恢复执行,这段时间内,线程无法执行其他任务。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        println("Coroutine started")
        delay(1000) // 挂起操作
        println("Coroutine resumed")
    }
    println("Main function")
}

delay的挂起操作,虽然会让当前协程执行暂停1秒后恢复执行,为什么是挂起,而不是阻塞呢,就是delay的挂起,对于它所在的线程并不会阻塞,Main function依旧会执行打印,然后打印started, 并暂停1秒打印resumed。

dispatcher

调度器,负责协程的上下文切换的对象。它决定了协程在哪个线程或线程池中运行,Dispatcher的实现依赖于底层的线程池和任务队列机制,以实现线程间的切换和任务调度。

常见的Dispatcher类型:

  • Dispatchers.Default:使用共享的后台线程池,适用于CPU密集型任务。
  • Dispatchers.IO:使用共享的线程池,适用于I/O密集型任务,如文件操作和网络请求。
  • Dispatchers.Main:在主线程上执行,通常用于更新UI(在Android开发中常用)。
object MainDispatcher : CoroutineDispatcher() {
    private val mainHandler = Handler(Looper.getMainLooper())

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        mainHandler.post(block)
    }
}

调度器设置的主线程的调度器,就是依赖Android的Handler和Looper来完成的切换。

object DefaultDispatcher : CoroutineDispatcher() {
    private val executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        executor.execute(block)
    }
}

object IODispatcher : CoroutineDispatcher() {
    private val executor = Executors.newCachedThreadPool()

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        executor.execute(block)
    }
}

而Default 和 IO都是使用一个共享的线程池,它们的调度原理都是通过创建一个新的Continuation,并使用新的上下文,提交任务到新的调度器来实现的切换。

suspend fun <T> withContext(context: CoroutineContext, block: suspend () -> T): T {
    // 获取当前协程的 Continuation
    val continuation = suspendCoroutineUninterceptedOrReturn<T> { cont ->
        // 创建一个新的 Continuation,使用新的上下文
        val newContinuation = cont.intercepted().withContext(context)
        // 提交任务到新的调度器
        context[ContinuationInterceptor]?.dispatch(context, Runnable {
            newContinuation.resumeWith(Result.success(block()))
        })
        COROUTINE_SUSPENDED
    }
    return continuation
}

resumeWith是Continuation接口的一部分,用于恢复挂起的协程,还会根据协程的上下文(调度器)决定在哪个线程恢复执行,这个之前没有提到。它接收一个Result对象作为参数,表示协程的结果或异常。

Result可以保存协程的结果和异常,这也就是为什么使用Async的方式,可以通过try…catch的方式捕获deferred.await的异常。协程的异常虽然会被保存在Result中,并传播到外层中抛出,但是异常不会自动传播给到父协程中去处理

父协程:在已有的协程内部启动一个协程,外部的协程就是父协程,内部的就是子协程。

异常

刚刚有提到在协程中的异常问题,和线程一样,你无法协程外部去捕获协程内部的异常,但是async可以通过wait的方式来捕获。

如果父协程有两个子协程,其中一个协程出现异常,父协程和其他子协程都会被取消。因为默认的CoroutineScope和Job会将异常传播给父协程的Job,导致父协程和其他子协程被取消,但异常不会直接抛出到父协程的代码中。

fun main() = runBlocking {
    val parentJob = launch {
        val child1 = launch {
            println("Child 1 is running")
            delay(1000)
            println("Child 1 completed")
        }

        val child2 = launch {
            println("Child 2 is running")
            throw RuntimeException("Error in Child 2")
        }

        try {
            joinAll(child1, child2)
        } catch (e: Exception) {
            println("Caught exception: ${e.message}")
        }
    }

    parentJob.join()
    println("Parent job completed")
}

print:
Child 1 is running
Child 2 is running
Caught exception: Error in Child 2
Parent job completed

但是这有一个例外,就是当你使用的是supervisorScope或 SupervisorJob的时候,异常不会传播到父协程,因此其他的协程都可以正常完成。

fun main() = runBlocking {
    supervisorScope {
        val child1 = launch {
            println("Child 1 is running")
            delay(1000)
            println("Child 1 completed")
        }

        val child2 = launch {
            println("Child 2 is running")
            throw RuntimeException("Error in Child 2")
        }

        try {
            joinAll(child1, child2)
        } catch (e: Exception) {
            println("Caught exception: ${e.message}")
        }
    }

    println("Parent job completed")
}
print:
Child 1 is running
Child 2 is running
Child 1 completed
Parent job completed

除了之前提到的async#wait的方式可以让父协程捕获子协程的异常,还可以通过supervisorScope

fun main() = runBlocking {
    supervisorScope {
        try {
            val childJob = launch {
                println("Child is running")
                throw RuntimeException("Error in child coroutine")
            }
            childJob.join() // 等待子协程完成,并捕获异常
        } catch (e: Exception) {
            println("Caught exception: ${e.message}")
        }
    }

    println("Parent job completed")
}

join是一个挂起函数,可以让异常传播到Job中,等挂起点恢复,再被父协程捕获。

三、总结

这次总结没什么好讲的了,也算是深入的了解了一下协程和调度器的原理。之后希望能够对协程还有更进一步的认识。

  • 16
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值