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],在正式撸码之前先对它有一个全面理论化的了解: 协程的定义: 协和通过将复杂性放入 ...
随机推荐
- 【GPU】如何两周内零经验手搓一个GPU | 美国工程师极限挑战 | 重写三次 | CUDA | SIMD | ISA指令集 | Verilog | OpenLane
地址: https://www.youtube.com/watch?v=FTh-c2ek6PU
- 工作站CPU超频,通过测试软件的压力测试就OK了吗?终极测试还是要看具体的应用场景
最近想起超频试试自己年初入手的工作站,也是纯玩,超超频,看看是什么个体验,不过这个过程中还真有所学习. 由于电脑的主板,电源,散热等配的都比较高端,所以上来直接超频5.2G,电压选择为自动,不过再整个 ...
- vscode配置远程项目开发
0. vscode中安装remote development插件 (略) 1. 客户端进行配置 按键:ctrl+shift+p 在输入框中输入: Preferences: Open User Sett ...
- python语言绘图:绘制一组正态分布图
代码源自: https://github.com/PacktPublishing/Bayesian-Analysis-with-Python ============================= ...
- [COCI 2023/2024 #3] Slučajna Cesta 题解
前言 期望套期望,很有意思.来一发考场首 A,近 \(\Theta(n)\) 的算法. 题目链接:洛谷. 题意简述 一棵树,每条边随机设有方向.对于所有 \(i\),从 \(i\) 开始随机游走,直到 ...
- IDEA-实时显示当前所在类及方
有时候用GIT对比文件修改过大,没法同步修改的时候,这个操作方式就变得极为有用.接下来就是是实操. 1 view 2 Active Editor 3 勾上show Breadcrumbs 4 看IDE ...
- 使用 Nuxt 3 的 defineRouteRules 进行页面级别的混合渲染
title: 使用 Nuxt 3 的 defineRouteRules 进行页面级别的混合渲染 date: 2024/8/12 updated: 2024/8/12 author: cmdragon ...
- 2024 睿抗机器人开发者大赛CAIP-编程技能赛-本科组(省赛)
2024 睿抗机器人开发者大赛CAIP-编程技能赛-本科组(省赛) RC-u1 热҈热҈热҈ #include<bits/stdc++.h> using namespace std; us ...
- NCP1207A笔记
uc3844d8 NCP1207A实现一个标准的电流模式结构,关断时间由峰值电流设置决定:铁芯复位检测则触发开启事件. 变压器铁芯检测:无论什么操作都会保证临界操作.因此,几乎没有一次开关接通损耗和二 ...
- AvaloniaChat—从源码构建指南
AvaloniaChat介绍 一个使用大型语言模型进行翻译的简单应用. 我自己的主要使用场景 在看英文文献的过程中,比较喜欢对照着翻译看,因此希望一边是英文一边是中文,虽然某些软件已经自带了翻译功能, ...