共享的可变状态与并发

  协程可⽤多线程调度器(⽐如默认的 Dispatchers.Default)并发执⾏。这样就可以提出所有常⻅的并发 问题。主要的问题是同步访问共享的可变状态。协程领域对这个问题的⼀些解决⽅案类似于多线程领域 中的解决⽅案,但其它解决⽅案则是独⼀⽆⼆的。

问题

  我们启动⼀百个协程,它们都做⼀千次相同的操作。我们同时会测量它们的完成时间以便进⼀步的⽐较

suspend fun massiveRun(action: suspend () -> Unit) {
val n = 100 // 启动的协程数量
val k = 1000 // 每个协程重复执⾏同⼀动作的次数
val time = measureTimeMillis {
coroutineScope { // 协程的作⽤域
repeat(n) {
launch {
repeat(k) { action() }
}
}
}
}
println("Completed ${n * k} actions in $time ms")
}

  我们从⼀个⾮常简单的动作开始:使⽤多线程的 Dispatchers.Default 来递增⼀个共享的可变变量

var counter = 0
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
counter++
}
}
println("Counter = $counter")
}

  这段代码最后打印出什么结果?它不太可能打印出“Counter = 100000”,因为⼀百个协程在多个线程中 同时递增计数器但没有做并发处理。

volatile ⽆济于事

  有⼀种常⻅的误解:volatile 可以解决并发问题。让我们尝试⼀下:

@Volatile // 在 Kotlin 中 `volatile` 是⼀个注解
var counter = 0
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
counter++
}
}
println("Counter = $counter")
}

  这段代码运⾏速度更慢了,但我们最后仍然没有得到“Counter = 100000”这个结果,因为 volatile 变量 保证可线性化(这是“原⼦”的技术术语)读取和写⼊变量,但在⼤量动作(在我们的⽰例中即“递增”操 作)发⽣时并不提供原⼦性。

线程安全的数据结构

  ⼀种对线程、协程都有效的常规解决⽅法,就是使⽤线程安全(也称为同步的、可线性化、原⼦)的数据结 构,它为需要在共享状态上执⾏的相应操作提供所有必需的同步处理。在简单的计数器场景中,我们可 以使⽤具有 incrementAndGet 原⼦操作的 AtomicInteger 类:

val counter = AtomicInteger()
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
counter.incrementAndGet()
}
}
println("Counter = $counter")
}

  这是针对此类特定问题的最快解决⽅案。它适⽤于普通计数器、集合、队列和其他标准数据结构以及它 们的基本操作。然⽽,它并不容易被扩展来应对复杂状态、或⼀些没有现成的线程安全实现的复杂操作

以细粒度限制线程

  限制线程 是解决共享可变状态问题的⼀种⽅案:对特定共享状态的所有访问权都限制在单个线程中。它 通常应⽤于 UI 程序中:所有 UI 状态都局限于单个事件分发线程或应⽤主线程中。这在协程中很容易实 现,通过使⽤⼀个单线程上下⽂:

val counterContext = newSingleThreadContext("CounterContext")
var counter = 0
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
// 将每次⾃增限制在单线程上下⽂中
withContext(counterContext) {
counter++
}
}
}
println("Counter = $counter")
}

  这段代码运⾏⾮常缓慢,因为它进⾏了 细粒度 的线程限制。每个增量操作都得使⽤ [withContext(counterContext)] 块从多线程 Dispatchers.Default 上下⽂切换到单线程上下⽂。

以粗粒度限制线程

  在实践中,线程限制是在⼤段代码中执⾏的,例如:状态更新类业务逻辑中⼤部分都是限于单线程中。下 ⾯的⽰例演⽰了这种情况,在单线程上下⽂中运⾏每个协程。

val counterContext = newSingleThreadContext("CounterContext")
var counter = 0
fun main() = runBlocking {
// 将⼀切都限制在单线程上下⽂中
withContext(counterContext) {
massiveRun {
counter++
}
}
println("Counter = $counter")
}

  这段代码运⾏更快⽽且打印出了正确的结果。

互斥

  该问题的互斥解决⽅案:使⽤永远不会同时执⾏的 关键代码块 来保护共享状态的所有修改。在阻塞的 世界中,你通常会为此⽬的使⽤ synchronized 或者 ReentrantLock 。在协程中的替代品叫做 Mutex 。它具有 lock 和 unlock ⽅法,可以隔离关键的部分。关键的区别在于 Mutex.lock() 是⼀个 挂起函数,它不会阻塞线程。 还有 withLock 扩展函数,可以⽅便的替代常⽤的 mutex.lock(); try { …… } finally { mutex.unlock() } 模式:

  

val mutex = Mutex()
var counter = 0
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
// ⽤锁保护每次⾃增
mutex.withLock {
counter++
}
}
}
println("Counter = $counter")
}

  此⽰例中锁是细粒度的,因此会付出⼀些代价。但是对于某些必须定期修改共享状态的场景,它是⼀个 不错的选择,但是没有⾃然线程可以限制此状态。

Actors

  ⼀个 actor 是由协程、被限制并封装到该协程中的状态以及⼀个与其它协程通信的 通道 组合⽽成的⼀ 个实体。⼀个简单的 actor 可以简单的写成⼀个函数,但是⼀个拥有复杂状态的 actor 更适合由类来表 ⽰。

  有⼀个 actor 协程构建器,它可以⽅便地将 actor 的邮箱通道组合到其作⽤域中(⽤来接收消息)、组合 发送 channel 与结果集对象,这样对 actor 的单个引⽤就可以作为其句柄持有。

  使⽤ actor 的第⼀步是定义⼀个 actor 要处理的消息类。Kotlin 的密封类很适合这种场景。我们使⽤ IncCounter 消息(⽤来递增计数器)和 GetCounter 消息(⽤来获取值)来定义 CounterMsg 密 封类。后者需要发送回复。CompletableDeferred 通信原语表⽰未来可知(可传达)的单个值,这⾥被⽤ 于此⽬的。

// 计数器 Actor 的各种类型
sealed class CounterMsg
object IncCounter : CounterMsg() // 递增计数器的单向消息
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // 携带回复的请求

  接下来我们定义⼀个函数,使⽤ actor 协程构建器来启动⼀个 actor:

// 这个函数启动⼀个新的计数器 actor
fun CoroutineScope.counterActor() = actor<CounterMsg> {
var counter = 0 // actor 状态
for (msg in channel) { // 即将到来消息的迭代器
when (msg) {
is IncCounter -> counter++
is GetCounter -> msg.response.complete(counter)
}
}
}

  main 函数代码很简单:

fun main() = runBlocking<Unit> {
val counter = counterActor() // 创建该 actor
withContext(Dispatchers.Default) {
massiveRun {
counter.send(IncCounter)
}
}
// 发送⼀条消息以⽤来从⼀个 actor 中获取计数值
val response = CompletableDeferred<Int>()
counter.send(GetCounter(response))
println("Counter = ${response.await()}")
counter.close() // 关闭该actor
}

  actor 本⾝执⾏时所处上下⽂(就正确性⽽⾔)⽆关紧要。⼀个 actor 是⼀个协程,⽽⼀个协程是按顺序 执⾏的,因此将状态限制到特定协程可以解决共享可变状态的问题。实际上,actor 可以修改⾃⼰的私有 状态,但只能通过消息互相影响(避免任何锁定)。

  actor 在⾼负载下⽐锁更有效,因为在这种情况下它总是有⼯作要做,⽽且根本不需要切换到不同的上下⽂。

注意,actor 协程构建器是⼀个双重的 produce 协程构建器。⼀个 actor 与它接收消息的通道相关
联,⽽⼀个 producer 与它发送元素的通道相关联。

  

kotlin协程——>共享的可变状态与并发的更多相关文章

  1. Kotlin 协程一 —— 全面了解 Kotlin 协程

    一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...

  2. Kotlin协程解析系列(上):协程调度与挂起

    vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...

  3. Kotlin协程第一个示例剖析及Kotlin线程使用技巧

    Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...

  4. Retrofit使用Kotlin协程发送请求

    Retrofit2.6开始增加了对Kotlin协程的支持,可以通过suspend函数进行异步调用.本文简单介绍一下Retrofit中协程的使用 导入依赖 app的build文件中加入: impleme ...

  5. Kotlin协程基础

    开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 我们已经通过第一个例子学会了启 ...

  6. Android Kotlin协程入门

    Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...

  7. rxjava回调地狱-kotlin协程来帮忙

    本文探讨的是在tomcat服务端接口编程中, 异步servlet场景下( 参考我另外一个文章),用rxjava来改造接口为全流程异步方式 好处不用说 tomcat的worker线程利用率大幅提高,接口 ...

  8. 多道技术 进程 线程 协程 GIL锁 同步异步 高并发的解决方案 生产者消费者模型

    本文基本内容 多道技术 进程 线程 协程 并发 多线程 多进程 线程池 进程池 GIL锁 互斥锁 网络IO 同步 异步等 实现高并发的几种方式 协程:单线程实现并发 一 多道技术 产生背景 所有程序串 ...

  9. Kotlin协程通信机制: Channel

    Coroutines Channels Java中的多线程通信, 总会涉及到共享状态(shared mutable state)的读写, 有同步, 死锁等问题要处理. 协程中的Channel用于协程间 ...

  10. Kotlin协程重要概念详解【纯理论】

    在之前对Kotlin的反射进行了详细的学习,接下来进入一个全新的篇章,就是关于Koltin的协程[coroutine],在正式撸码之前先对它有一个全面理论化的了解: 协程的定义: 协和通过将复杂性放入 ...

随机推荐

  1. 【Vue】Re19 Promise

    一.概述 Promise是异步编程的解决方案 异步事件的处理: 封装的异步请求函数不能立即获取结果, 通常会传入另外一个函数,在请求成功的时候将数据通过传入的函数回调出去 如果只是一个简单的请求,那么 ...

  2. 【Java】API SecureRandom 安全随机

    之前学习的Random工具类只是一个伪随机数类... @Test public void secureRandom() throws Exception { // 个不可预测的安全的随机数 // 无法 ...

  3. 强化学习:连续控制问题中Actor-Critic算法的linear baseline

    最近在看连续控制问题,看到了一个Actor-Critic算法中手动扩展features和设置linear baseline的方法,这些方法源自论文:<Benchmarking Deep Rein ...

  4. ArgoWorkflow 教程(一)--DevOps 另一选择?云原生 CICD 初体验

    本文主要记录了如何使用 ArgoWorkflow 构建流水线,以及 ArgoWorkflow 中 的 Workflow.Template 等概念模型. 本文主要分析以下问题: 1)如何创建流水线 2) ...

  5. vue前端自适应布局,一步到位所有自适应

    vue前端自适应布局,一步到位所有自适应 页面展示 实现内容 1,左右布局 左侧固定宽带,右侧自适应剩余的宽度. 中间一条分割线,可以拖拉,自适应调整左右侧的宽度. 左侧的高度超长自动出现横向滚动条, ...

  6. 【CDQ分治】[P5094 [USACO04OPEN] MooFest G 加强版

    P5094 [USACO04OPEN] MooFest G 加强版 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) #include <bits/stdc++.h> us ...

  7. JavaScript设计模式样例六 —— 抽象工厂模式

    抽象工厂模式(Abstract Factory Pattern) 定义:抽象工厂模式提供了一种方式,可以将一组具有同一主题的单独的工厂封装起来.或者说,是其他工厂的工厂.目的:提供一个创建一系列相关或 ...

  8. pom阿里加速镜像地址

    <repositories> <repository> <id>alimaven</id> <name>aliyun maven</n ...

  9. LaTeX 编译中文文档

    介绍 LaTeX 原生不支持中文.为了添加中文的功能,我们需要引入宏包.XeLaTeX 原生支持中文.不过由于默认使用的字体是英文字体,我们需要设置中文字体之后才能用.不过由于一些原因,在使用 LaT ...

  10. Python批量分割Excel后逐行做差、合并文件的方法

      本文介绍基于Python语言,针对一个文件夹下大量的Excel表格文件,基于其中每一个文件,首先依据某一列数据的特征截取我们需要的数据,随后对截取出来的数据逐行求差,并基于其他多个文件夹中同样大量 ...