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],在正式撸码之前先对它有一个全面理论化的了解: 协程的定义: 协和通过将复杂性放入 ...
随机推荐
- tf.metrics 使用过程中发现的一些问题
起因是看到了这么一个帖子: http://www.cocoachina.com/cms/wap.php?action=article&id=86347 简短来说就是下面的代码 运行起来结果 ...
- 【转载】 ReLu(Rectified Linear Units)激活函数
原文地址: https://www.cnblogs.com/neopenx/p/4453161.html ============================== 论文参考:Deep Sparse ...
- 【转载】 【报错】ImportError: cannot import name 'downsample' —— lasagne模块 调用 theano 报错
原网址: https://blog.csdn.net/kz_java/article/details/125030733 ======================================= ...
- JUC高并发编程(三)之模拟接口压力测试
1.背景 接口压力测试是产品上线前很重要的一项测试,我们可以使用很多开源工具测试, 当然我们也可以简单的写一个多线程并发测试案例 2.代码 controller接口 /** * 查询订单 * * @r ...
- css 样式 element.style 覆盖问题
问题: 我们在写网页定制样式的时候发现展示效果跟我们预想的不一样? 打开F12一看原来是element.style 覆盖的我定义的效果. 解决: 只要在定义的内容后面加上 !important 就行啦 ...
- python分布式事务方案(一)tcc
python分布式事务方案(一)tcc 随着单体应用的拆分以及服务化的流行,现在分布式事务已经比较常见,分布式事务理论ACID.CAP.BASE等我就不说了,现在就直接说一下一种常见的解决方案-tcc ...
- 2023 ICPC 香港
gym 开场发现 E 是传统数据结构题很高兴,不过先跳了.F 知道相邻两段的长度差 \(\le1\),以为最终每段长度只有 \(\lfloor\frac{n}{m+1}\rfloor,\lceil\f ...
- C语言基础- Hello World
第一个C语言程序 Hello World # include <stdio.h> //#关键标识符,表示用用头文件:include:引入都文件关键字 // stdio.h:系统标准输入.输 ...
- Transformer模型:Position Embedding实现
在自然语言处理(NLP)中,Transformer 模型是一个非常重要的里程碑,它通过自注意力(self-attention)机制极大地提高了处理序列数据的能力.在 Transformer 模型中,词 ...
- 树莓派CM4(四):树莓派镜像替换内核
树莓派镜像替换内核 1. 为什么要替换内核 树莓派官方提供的镜像中,自带的内核版本为6.6.31 然而github上提供的内核源码为6.6.40,有些微差别 此外,后续很有可能进行内核裁剪定制等工作, ...