说一说Kotlin协程中的同步锁——Mutex

embedded/2024/9/20 7:23:40/ 标签: kotlin

目录

  • 前言
  • Mutex
    • withLock
  • lock
    • Empty
    • LockedQueue
  • unlock
  • LockCont
  • 总结

前言

在多线程并发的情况下会很容易出现同步问题,这时候就需要使用各种锁来避免这些问题,在java开发中,最常用的就是使用synchronized。kotlin的协程也会遇到这样的问题,因为在协程线程池中会同时存在多个运行的Worker,每一个Worker都是一个线程,这样也会有并发问题。

虽然kotlin中也可以使用synchronized,但是有很大的问题。因为synchronized当获取不到锁的时候,会阻塞线程,这样这个线程一段时间内就无法处理其他任务,这不符合协程的思想。为此,kotlin提供了一个协程中可以使用的同步锁——Mutex

Mutex

Mutex使用起来也非常简单,只有几个函数lock、unlock、tryLock,一看名字就知道是什么。还有一个holdsLock,就是返回当前锁的状态。

这里要注意,lock和unlock必须成对出现,tryLock返回true的之后也必须在使用完执行unlock。这样使用的时候就比较麻烦,所以kotlin还提供了一个扩展函数withLock,它与synchronized类似,会在代码执行完成或异常的时候自动释放锁,这样就避免了忘记释放锁导致程序出错的情况。

withLock

withLock的代码如下:

kotlin">public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T {contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE)}lock(owner)try {return action()} finally {unlock(owner)}
}

代码非常简单,就是先lock一下,然后执行代码,最终在finally中释放锁,这样就保证了锁一定会被释放。

lock

这样一看mutex好像跟synchronized或其他java的锁差不多,那么为什么它是如何解决线程阻塞的问题呢。

这就要从lock和unlock的流程中来看,先来看看lock:

kotlin">public override suspend fun lock(owner: Any?) {// fast-path -- try lockif (tryLock(owner)) return// slow-path -- suspendreturn lockSuspend(owner)
}

先是通过tryLock来获取锁,如果获取到了就直接返回执行代码。重点来看获取不到是如何处理的,获取不到的时候会执行lockSuspend,它的代码如下:

kotlin">private suspend fun lockSuspend(owner: Any?) = suspendCancellableCoroutineReusable<Unit> sc@ { cont ->var waiter = LockCont(owner, cont)  //1_state.loop { state ->when (state) {is Empty -> {if (state.locked !== UNLOCKED) {  //2_state.compareAndSet(state, LockedQueue(state.locked)) //3} else {// try lockval update = if (owner == null) EMPTY_LOCKED else Empty(owner)if (_state.compareAndSet(state, update)) { // lockedcont.resume(Unit) { unlock(owner) } //4return@sc}}}is LockedQueue -> {val curOwner = state.ownercheck(curOwner !== owner) { "Already locked by $owner" }state.addLast(waiter)  //5if (_state.value === state || !waiter.take()) {  //6// added to waiter listcont.removeOnCancellation(waiter)return@sc}waiter = LockCont(owner, cont)return@loop}is OpDescriptor -> state.perform(this) // helpelse -> error("Illegal state $state")}}
}

可以看到这个函数是被suspend修饰的,所以这个是可挂起的函数,当执行到这里的时候线程就被挂起了,如果没有立刻恢复,而且有其他任务,那么线程就可以先执行其他任务,这样就不会阻塞住了。那么是如何恢复的。

函数一开始创建了一个LockCont对象waiter,这个是后面的关键,不过现在还用不到。

Empty

继续看根据不同的状态执行不同的代码,先看看Empty(等待列表为空)状态,再判断一下当前是否加锁(代码2),如果不是非加锁则将状态设置为LockedQueue状态(代码3);如果当前是非加锁,则获取锁,获取到之后执行resume来唤醒线程来执行后续代码(代码4),这种情况基本就是立刻获取到锁,所以不在这里细说了。

上面说了如果等待列表为空并且无法立刻获取锁,就会切换到LockedQueue状态(代码3),所以只要当前无法获取锁,最终都会进行LockedQueue状态,那么来看看这个状态怎么处理的。

LockedQueue

这个状态会就将函数一开始创建的waiter添加到state中(代码5),然后还是再判断一次当前状态,因为这时候可能锁的状态已经改变了,如果没有变则直接就返回了。

注意看到每个状态里,都会反复的校验当前锁的状态。

可以看到在LockedQueue这个流程结束后并没有恢复线程,线程则一直是挂起状态,所以在恢复之前线程是可以处理其他事务的。

那么线程何时恢复?

unlock

来看看unlock代码:

kotlin">override fun unlock(owner: Any?) {_state.loop { state ->when (state) {is Empty -> {...}is OpDescriptor -> state.perform(this)is LockedQueue -> {if (owner != null)check(state.owner === owner) { "Mutex is locked by ${state.owner} but expected $owner" }val waiter = state.removeFirstOrNull()  //1if (waiter == null) {...} else {if ((waiter as LockWaiter).tryResumeLockWaiter()) { //2state.owner = waiter.owner ?: LOCKEDwaiter.completeResumeLockWaiter() //3return}}}else -> error("Illegal state $state")}}
}

上面我们将waiter放入了等待队列中,这时候状态是LockedQueue,所以在unlock函数中我们直接看这个状态的代码。

代码1处从state中取出第一个元素,即waiter。前一个释放锁之后,就会把锁分配给这个waiter。然后在代码2处执行了它的tryResumeLockWaiter函数,如果返回false,还会执行它的completeResumeLockWaiter函数。

LockCont

上面知道waiter是一个LockCont对象,我们来看看它的源码:

kotlin">private inner class LockCont(owner: Any?,private val cont: CancellableContinuation<Unit>
) : LockWaiter(owner) {override fun tryResumeLockWaiter(): Boolean {if (!take()) return falsereturn cont.tryResume(Unit, idempotent = null) {unlock(owner)} != null}override fun completeResumeLockWaiter() = cont.completeResume(RESUME_TOKEN)...
}

可以看到在tryResumeLockWaiter函数中会执行cont的tryResume来尝试唤醒它对应的线程来执行代码。

如果这个动作没有成功,最后会在completeResumeLockWaiter函数中执行cont的completeResume来唤醒线程。

总结

Mutex的内部逻辑其实并不复杂,如果获取不到锁则会挂起线程并加入到等待队列中,等获取到锁的时候在唤醒线程来执行代码。而这段时间内线程,或者说Worker可以执行其他任务,这样不会阻塞线程,最大的利用了线程的资源,这就很kotlin

所以大家在处理协程的同步问题的时候,尽量使用Mutex这种Kotlin专门为协程开发的工具,这样才能更好的发挥协程的能力。


http://www.ppmy.cn/embedded/31506.html

相关文章

PPT基础

5种ppt仅可读形式 Ⅰ 开始选项卡 1.【幻灯片】组中&#xff1a;新建幻灯片&#xff0c;从大纲中导入幻灯片&#xff1b;修改幻灯片的版式&#xff1b;节&#xff08;新增节&#xff0c;重命名节&#xff09;。 2.【字体】组中&#xff1a;设置字体&#xff0c;字体大小&…

根据顶层的id递归查询出全部子节点

效果图 根据输入的id为2查询出所有的红色框起来的节点 mapper接口 TSystemOrg getOrgByorgId(String orgId); List<TSystemOrg> getOrgListByParentId(String parentId);mapper.xml <!--根据id查询org--> <select id"getOrgByorgId" resultType&…

MFC列表控件用ADO添加数据实例

1、本程序基于前期我的博客文章《MFC用ADO连接ACESS数据库实例(免费源码下载)》 程序功能通过编辑框、组合框实时将数据写入ACESS数据库并在列表控件上显示。 2、在主界面资源视图上加上一个按钮控件、两个静态文本、一个编辑框IDC_EDIT1变量名name、一个组合框IDC_COMBO1变量名…

python实验一 简单的递归应用

实验一 实验题目 1、兔子繁殖问题(Fibonacci’s Rabbits)。一对兔子从出生后第三个月开始&#xff0c;每月生一对小兔子。小兔子到第三个月又开始生下一代小兔子。假若兔子只生不死&#xff0c;一月份抱来一对刚出生的小兔子&#xff0c;问一年中每个月各有多少只兔子。 &…

Ubuntu启动后进入GRUB故障-Minimal BASH like line editing is supported.

目录 1.问题描述 2.解决方案 2.1 临时性办法 2.2 工具永久性修复 总结 1.问题描述 PC安装Ubuntu系统第二天重启后提示GUN GRUB version 2.04&#xff0c;之前是WindowsOS装Ubuntu后无法进入图形界面。具体原因据网友提供线索据说是由于在Windows上进行更新/重装/修改了引…

tomcat打开乱码修改端口

将UTF-8改成GBK 如果端口冲突&#xff0c;需要修改tomcat的端口

C语言 | Leetcode C语言题解之第60题排列序列

题目&#xff1a; 题解&#xff1a; char* getPermutation(int n, int k) {int factorial[n];factorial[0] 1;for (int i 1; i < n; i) {factorial[i] factorial[i - 1] * i;}--k;char* ans malloc(n 1);ans[n] \0;int valid[n 1];for (int i 0; i < n; i) {val…

拼多多怎么推广才有自然流量

在拼多多平台上获取自然流量&#xff0c;商家可以采取以下几种策略&#xff1a; 拼多多推广可以使用3an推客。3an推客&#xff08;CPS模式&#xff09;给商家提供的营销工具&#xff0c;由商家自主设置佣金比例&#xff0c;激励推广者去帮助商家推广商品链接&#xff0c;按最…

49. 【Android教程】HTTP 使用详解

在你浏览互联网的时候&#xff0c;绝大多数的数据都是通过 HTTP 协议获取到的&#xff0c;也就是说如果你想要实现一个能上网的 App&#xff0c;那么就一定会和 HTTP 打上交道。当然 Android 发展到现在这么多年&#xff0c;已经有很多非常好用&#xff0c;功能非常完善的网络框…

算法--分治法

分治法是一种算法设计策略&#xff0c;它将一个复杂的问题分解成两个或多个相同或相似的子问题&#xff0c;直到这些子问题可以简单地直接解决。然后&#xff0c;这些子问题的解被合并以产生原始问题的解。 分治法通常遵循以下三个步骤&#xff1a; 分解&#xff1a;将原问题…

前端页面平滑过渡解决方案

一、问题产生 在使用图片作为页面背景时&#xff0c;无法使用transtion进行平滑过渡&#xff0c;直接切换背景又会降低使用体验。 二、解决方式 使用clip-path对背景图片裁剪配合transtion实现平滑过渡的效果 三、效果展示 网址&#xff1a;ljynet.com 四、实现方式 tem…

SQL LPAD函数使用

Oracle SQL 中的 LPAD 函数是一个用于格式化字符串的函数&#xff0c;它会在给定字符串的左侧填充指定的字符&#xff0c;直到字符串达到指定的长度。这个函数在数据库查询中非常有用&#xff0c;尤其是在需要对输出进行格式化时&#xff0c;比如在报表中。 LPAD 函数的语法 …

如何确定控制器的采样频率?

确定控制器的采样频率通常涉及到系统的要求、控制算法的稳定性以及硬件的性能等因素。在确定采样频率时&#xff0c;需要平衡这些因素&#xff0c;以确保系统的稳定性和性能。 在LabVIEW中进行分析时&#xff0c;可以按照以下步骤进行&#xff1a; 确定系统要求&#xff1a;首…

古典密码学简介

目录 C. D. Shannon: 一、置换密码 二、单表代替密码 ① 加法密码 ② 乘法密码 ③密钥词组代替密码 三、多表代替密码 代数密码 四、古典密码的穷举分析 1、单表代替密码分析 五、古典密码的统计分析 1、密钥词组单表代替密码的统计分析 2、英语的统计规…

XYCTF2024 RE ez unity 复现

dll依然有加壳 但是这次global-metadata.dat也加密了&#xff0c;原工具没办法用了&#xff0c;不过依然是可以修复的 a. 法一&#xff1a;frida-il2cpp-bridge 可以用frida-il2cpp-bridge GitHub - vfsfitvnm/frida-il2cpp-bridge: A Frida module to dump, trace or hijac…

css实现瀑布流布局

瀑布流布局也可以通过纯CSS来实现&#xff0c;使用CSS的column属性可以实现多列布局。下面是一个使用纯CSS实现瀑布流布局的示例&#xff1a; <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"…

Opecv-Python常用算子库(总结)

文章目录 1 常用算子梗概2 实际项目中的总结 1 常用算子梗概 1.1 读取图像 cv2.imread(filename, flags) 1.2 显示图像 cv2.imshow(winname, mat) 1.3 保存图像 cv2.imwrite(filename, mat) 1.4 改变图像大小 cv2.resize(src, dsize, dstNone, fxNone, fyNone, interpolationN…

旅游新策略,共享与补贴助力地方经济繁荣

在当前的经济环境中&#xff0c;旅游业对于地方经济增长的重要性日益凸显。各个城市都在积极探索增加旅游流量的方法&#xff0c;以刺激本地经济的增长。 例如&#xff0c;淄博政府通过政策推动和合作模式&#xff0c;成功吸引了大量游客&#xff0c;这成为了一个成功的案例。…

商业银行终端安全管理创新与实践

文章目录 前言一、终端使用和管理现状二、终端面临的安全风险1、传统的终端安全工具无法有效识别新型威胁2、黑客攻击的目标重心瞄向终端三、终端安全防护技术的探索和实践1、远程办公场景首次尝试基于威胁情报技术的木马防护措施,取得良好成效2、自研终端数字化管控系统,提升…

openGauss学习笔记-274 openGauss性能调优-实际调优案例03-建立合适的索引

文章目录 openGauss学习笔记-274 openGauss性能调优-实际调优案例03-建立合适的索引274.1 现象描述274.2 优化分析 openGauss学习笔记-274 openGauss性能调优-实际调优案例03-建立合适的索引 274.1 现象描述 查询与销售部所有员工的信息&#xff1a; SELECT staff_id,first_…