kotlin协程——>共享的可变状态与并发
共享的可变状态与并发
协程可⽤多线程调度器(⽐如默认的 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协程——>共享的可变状态与并发的更多相关文章
- Kotlin 协程一 —— 全面了解 Kotlin 协程
一.协程的一些前置知识 1.1 进程和线程 1.1.1基本定义 1.1.2为什么要有线程 1.1.3 进程与线程的区别 1.2 协作式与抢占式 1.2.1 协作式 1.2.2 抢占式 1.3 协程 二 ...
- Kotlin协程解析系列(上):协程调度与挂起
vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...
- Kotlin协程第一个示例剖析及Kotlin线程使用技巧
Kotlin协程第一个示例剖析: 上一次https://www.cnblogs.com/webor2006/p/11712521.html已经对Kotlin中的协程有了理论化的了解了,这次则用代码来直 ...
- Retrofit使用Kotlin协程发送请求
Retrofit2.6开始增加了对Kotlin协程的支持,可以通过suspend函数进行异步调用.本文简单介绍一下Retrofit中协程的使用 导入依赖 app的build文件中加入: impleme ...
- Kotlin协程基础
开发环境 IntelliJ IDEA 2021.2.2 (Community Edition) Kotlin: 212-1.5.10-release-IJ5284.40 我们已经通过第一个例子学会了启 ...
- Android Kotlin协程入门
Android官方推荐使用协程来处理异步问题.以下是协程的特点: 轻量:单个线程上可运行多个协程.协程支持挂起,不会使正在运行协程的线程阻塞.挂起比阻塞节省内存,且支持多个并行操作. 内存泄漏更少:使 ...
- rxjava回调地狱-kotlin协程来帮忙
本文探讨的是在tomcat服务端接口编程中, 异步servlet场景下( 参考我另外一个文章),用rxjava来改造接口为全流程异步方式 好处不用说 tomcat的worker线程利用率大幅提高,接口 ...
- 多道技术 进程 线程 协程 GIL锁 同步异步 高并发的解决方案 生产者消费者模型
本文基本内容 多道技术 进程 线程 协程 并发 多线程 多进程 线程池 进程池 GIL锁 互斥锁 网络IO 同步 异步等 实现高并发的几种方式 协程:单线程实现并发 一 多道技术 产生背景 所有程序串 ...
- Kotlin协程通信机制: Channel
Coroutines Channels Java中的多线程通信, 总会涉及到共享状态(shared mutable state)的读写, 有同步, 死锁等问题要处理. 协程中的Channel用于协程间 ...
- Kotlin协程重要概念详解【纯理论】
在之前对Kotlin的反射进行了详细的学习,接下来进入一个全新的篇章,就是关于Koltin的协程[coroutine],在正式撸码之前先对它有一个全面理论化的了解: 协程的定义: 协和通过将复杂性放入 ...
随机推荐
- 【Java】项目采用的设计模式案例
先说一下业务需要: 做电竞酒店后台系统,第一期功能有一个服务申请的消息通知功能 就是酒店用户在小程序点击服务功能,可以在后台这边查到用户的服务需要 原本设计是只需要一张表存储这些消息,但是考虑设计是S ...
- 【Java】利用反射更改String的字符
问题: 在不改变s变量引用的String对象的情况下,输出打印"abcd" /*** * * @param args * @return void * @author cloud9 ...
- 【Keepalived】KP + NGINX 多机热备学习
案例搭建 环境是三台机器,两台也可以 最后一个IP是测试的VIP 192.168.124.21 centos6-1 192.168.124.22 centos6-2 192.168.124.23 ce ...
- python报错:Pip 20.3+ break proxy connection
参考: https://www.cnblogs.com/devilmaycry812839668/p/17872452.html =================================== ...
- 强化学习运行环境,atari 2600 游戏模拟器,atari-py库 —— 无法运行游戏,pacman,surround,报错: Segmentation fault (core dumped)
atari2600运行环境: https://github.com/openai/atari-py 安装环境,以及导入 rom文件这里不进行介绍(前文已介绍): 测试游戏环境rom文件能否正常加载如内 ...
- 多节点高性能计算GPU集群的构建
建议参考原文: https://www.volcengine.com/docs/6535/78310 ============================================= 一直都 ...
- 解密prompt系列35. 标准化Prompt进行时! DSPy论文串烧和代码示例
一晃24年已经过了一半,我们来重新看下大模型应用中最脆弱的一环Prompt Engineering有了哪些新的解决方案.这一章我们先看看大火的DSPy框架,会先梳理DSPy相关的几篇核心论文了解下框架 ...
- 图扑 HT for Web 轻松构建组态拓扑结构
在现代的数据可视化和网络管理中,拓扑图是一种非常重要的工具.它可以直观地展示节点(Node)和节点之间的关系(Edge).无论是在 2D 还是 3D 环境中,拓扑图都可以帮助我们更好地理解和管理复 ...
- 如何快速在 Apache DolphinScheduler 新扩展一个任务插件?
作者 | 代立冬 编辑 | Debra Chen Apache DolphinScheduler 是现代数据工作流编排平台,具有非常强大的可视化能力,DolphinScheduler 致力于使数据工程 ...
- 批量删除git tag
批量删除远程tag $ git ls-remote -t --refs -q | awk '{print ":"$2}' | xargs git push origin To ss ...