“协程是轻量级的线程”,相信你不止一次听过这句话。但是你真的了解其中的含义吗?恐怕答案是否定的。下面的内容将告诉你协程在Android运行时是如何运行的,它们和线程有什么关系,以及在使用Java编程语言线程模型时遇到的并发问题。协程和线程协程旨在简化异步执行的代码。对于Android运行时中的协程,lambda表达式的代码块在专用线程中执行。例如,示例中的斐波那契操作//在后台线程中计算第十个斐波那契数{/*...*/}上面异步协程的代码块会分发到协程库管理的线程池中执行,实现了同步和阻塞斐波那契??值计算并将结果存入内存。上例中的线程池属于Dispatchers.Default。这个代码块会在以后某个时间在线程池中的一个线程中执行,具体执行时间取决于线程池的策略。注意,由于上面的代码不包含挂起操作,所以会在同一个线程中执行。协程有可能在不同的线程中执行,例如将执行部分移动到不同的调度程序,或者使用线程池将具有挂起操作的代码包含在调度程序中。如果不使用协程,也可以使用线程自己实现类似的逻辑。代码如下//创建一个有4个线程的线程池valexecutorService=Executors.newFixedThreadPool(4)//安排在其中一个线程中执行代码executorService.execute{valfibonacci10=synchronousFibonacci(10)saveFibonacciInMemory(10,fibonacci10)}虽然可以自己实现线程池管理,但是我们还是推荐使用协程作为Android开发中首选的异步实现方案,它内置了取消机制,可以提供更方便的异常捕获和结构化并发,可以减少类似的几率内存泄漏,并且与Jetpack库的集成度更高。它是如何工作的从您创建协程到代码由线程执行时会发生什么?当您使用标准协程构建器创建协程时,您可以指定协程运行的CoroutineDispatcher。如果不指定,系统将默认使用Dispatchers。默认。CoroutineDispatcher会负责将协程的执行分配给具体的线程。在底层调用CoroutineDispatcher时,会调用封装Continuation的interceptContinuation方法(比如这里的coroutine)来拦截协程。这个过程的前提是CoroutineDispatcher实现了CoroutineInterceptor接口。如果你看过我之前写的底层如何实现协程的文章,你应该已经知道编译器创建了一个状态机,状态机的相关信息(比如接下来做什么)存储在Continuation对象中。一旦Continuation对象需要在另一个Dispatcher中执行,DispatchedContinuation的resumeWith方法将负责将协程调度到合适的Dispatcher。另外,在Java编程语言的实现中,继承自DispatchedTask抽象类的DispatchedContinuation也是Runnable接口的一种实现类型。因此,DispatchedContinuation对象也可以在线程中执行。好处是当指定CoroutineDispatcher时,协程会转为DispatchedTask,作为Runnable在线程中执行。那么创建协程的时候,dispatch方法是怎么调用的呢?使用标准协程构建器创建协程时,可以指定类型为CoroutineStart的开始参数。比如你可以设置协程只在需要的时候启动,那么你可以将参数设置为CoroutineStart.LAZY。默认情况下,系统使用CoroutineStart.DEFAULT基于CoroutineDispatcher调度执行。△协程的代码块在线程Dispatcher和线程池中执行示意图可以使用Executor.asCoroutineDispatcher()扩展函数将协程转化为CoroutineDispatcher,然后在线程池中任意一个线程池中执行协程应用。此外,您可以使用协程库的默认Dispatchers。您可以看到如何在createDefaultDispatcher方法中初始化Dispatchers.Default。默认情况下,使用DefaultScheduler。如果看Dispatcher.IO的实现代码,也是使用了DefaultScheduler,支持按需创建至少64个线程。Dispatchers.Default和Dispatchers.IO是隐式关联的,因为它们使用同一个线程池,这就引出了我们下一个话题,使用不同的分发器调用withContext会造成什么运行时开销?Thread和withContext的性能表现在Android运行时。如果运行的线程数大于CPU的可用核数,切换线程会带来一定的运行时开销。上下文切换并不容易!操作系统需要保存和恢复执行的上下文,CPU除了执行实际的应用程序功能外,还需要花时间调度线程。另外,线程中运行的代码阻塞时,也会引起上下文切换。如果上面的问题是针对线程的,那么在不同的Dispatcher中使用withContext会造成什么性能损失呢?幸运的是,线程池会帮助我们解决这些复杂的操作,它会尝试执行尽可能多的任务(这也是为什么在线程池中执行操作比手动创建线程更好的原因)。协程还受益于被安排在线程池中执行。正因为如此,协程不会阻塞线程,它们会暂停它们的工作,从而提高它们的效率。Java编程语言默认使用的线程池是CoroutineScheduler。它以最有效的方式将协程分配给工作线程。由于Dispatchers.Default和Dispatchers.IO使用相同的线程池,因此在它们之间进行切换可以最大限度地减少线程切换。协程库会优化这些切换调用,保持在同一个dispatcher和thread上,尽量走捷径。由于Dispatchers.Main在有UI的应用中通常属于不同的线程,在协程中切换Dispatchers.Default和Dispatchers.Main不会造成太大的性能损失,因为协程会挂起(比如在某个线程停止执行),然后被安排在另一个线程中继续执行。协程中的并发问题协程确实使异步编程更容易,因为它允许在不同线程上轻松安排操作。但另一方面,方便是一把双刃剑:由于协程运行在Java编程语言的线程模型之上,因此很难逃脱线程模型带来的并发问题。因此,您需要注意并尽量避免此问题。近年来,不变性等策略相对缓解了线程带来的问题。但是在某些场景下,不变性策略并不能完全避免问题。所有并发问题的根源都是状态管理!特别是在多线程环境中访问可变状态。在多线程应用程序中,执行操作的顺序是不可预测的。与编译器优化操作的顺序不同,线程不能保证按特定顺序执行,上下文切换随时可能发生。如果在访问可变状态时没有采取必要的预防措施,线程将访问陈旧数据、丢失更新或遇到资源争用问题等。请注意,这里讨论的可变状态和访问顺序不仅限于Java编程语言。它们还会影响其他平台上的协程执行。使用协程的应用程序本质上是多线程应用程序。使用协程并涉及可变状态的类必须采取措施使其可控,例如确保协程中代码访问的数据是最新的。这样不同的线程就不会互相干扰了。并发问题会导致潜在的错误,使您的应用程序难以调试和定位问题,甚至海森堡错误。这种类型的课程很常见。例如,这个类需要在内存中缓存用户的登录信息,或者在应用程序处于活动状态时缓存一些值。如果您粗心大意,可能会出现并发问题!使用withContext(defaultDispatcher)的挂起函数不能保证在同一个线程中执行。例如,我们有一个类需要缓存用户进行的交易。如果没有正确访问缓存,如下代码所示,会出现并发问题:transaction:Transaction)=//注意!访问缓存的操作不受保护!//会出现并发问题:线程会访问过期数据//会出现资源竞争问题)newList.add(transaction)transactionsCache.put(user,newList)}else{transactionsCache.put(user,listOf(transaction))}}}即使我们在这里谈论Kotlin,《Java 并发编程实践》byBrianGoetzforIt是理解本文主题和Java编程语言系统的绝佳参考资料。此外,Jetbrains还提供有关共享可变状态和并发性主题的文档。保护可变状态如何保护可变状态,或者找到合适的同步策略,取决于数据本身和相关操作。本节的内容激发了对可能的并发问题的关注,而不是简单地列出方法和API来保护可变状态。总而言之,这里有一些提示和API可以帮助您实现可变变量的线程安全。封装可变状态应该属于并封装在类中。该类应该集中状态访问操作,并根据应用场景使用同步策略来保护变量访问和修改操作。线程限制一种解决方案是将读取和写入操作限制在单个线程中。可以使用基于生产者-消费者模式的队列来实现对可变状态的访问。Jetbrains对此有很好的文档。避免重复工作Android运行时包含线程安全数据结构,可让您保护可变变量。例如,在计数器示例中,您可以使用AtomicInteger。再举个例子,为了保护上面代码中的Map,可以使用ConcurrentHashMap。ConcurrentHashMap是线程安全的,优化了map读写操作的吞吐量。请注意,线程安全数据结构不解决调用顺序问题,它们仅确保内存数据访问是原子的。当逻辑不太复杂时,他们可以避免使用锁。比如上面的transactionCache例子中就不能使用它们,因为它们之间的操作顺序和逻辑需要使用线程和访问保护。此外,当修改后的对象存储在这些线程安全数据结构中时,其中的数据需要保持不可变或受保护以避免资源争用问题。自定义解决方案如果您有需要同步的复杂操作,@Volatile和线程安全的数据结构将不会有效。内置的@Synchronized注释可能不够精细,无法达到预期效果。在这些情况下,您可能需要使用并发工具创建自己的同步机制,例如锁存器、信号量或屏障。在其他场景下,可以使用锁和互斥锁来无条件地保护多线程访问。Kotlin中的Mute包含挂起函数lock和unlock,可以手动控制保护协程的代码。扩展函数Mutex.withLock使用起来更方便:classTransactionsRepository(privatevaldefaultDispatcher:CoroutineDispatcher=Dispatchers.Default){//Mutex保护可变状态缓存privatevalcacheMutex=Mutex()privatevaltransactionsCache=mutableMapOf
