/ 今日科技快讯 /
9月1日,雷军社交媒体晒图:小米汽车新同学,今天开学。并公布了小米汽车公司名:小米汽车有限公司。雷军表示,小米汽车有限公司注册资金100亿,雷军担任法人代表。
/ 作者简介 /
本篇文章来自ahhsfj1991的投稿,文章主要分享了kotlin开发中通过Mutex解决多并发问题,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
/ 问题背景 /
如果我们同时开启多条线程去修改某一变量时,最终的结果可能不是我们所预期的。如下方的代码所示:
fun main(args: Array<String>) {
var count = 0
(0..1000).forEach { _ ->
Thread(Runnable {
count++
}).start()
}
println(count)
}
相信这段代码大家基本都看到过,这里不再介绍为什么会出现并发问题,解决方案也很多,在以前Java中大致有三个思路:
synchronized
AtomicInteger
ReentrantLock
第二种方法保证了原子性,在上述例子中是有用的,但是如果是一个代码块需要保证线程安全,就只能采用1和3两种方法保证线程安全了。这两种都是阻塞同步保证线程安全。
下面说一下kotlin中的协程,虽然官方号称协程是轻量级线程,但其实它并不是什么黑魔法,可以认为它就是利用kotlin的封装出一套符合协程思想的线程框架。所以它也是会有线程并发的问题,如下面这段代码,和传统的线程并发问题的示例代码非常类似,运行一下就知道,它的结果也是不符合预期的。
fun main() = runBlocking {
var count = 0
repeat(1000) {
GlobalScope.launch {
count++
}
}
println(count)
}
要解决上述代码的并发问题,传统的Java解决方案也是有效的。但是这里,要介绍一种kotlin的解决方案,它是非阻塞线程的Mutex。
fun main() = runBlocking {
val mutex = Mutex()
var count = 0
repeat(1000) {
GlobalScope.launch {
mutex.withLock {
count++
}
}
}
println(count)
}
Mutex使用起来是比较方便的,但是这里并不止步于此,我们从源码分析一下为什么它是非阻塞式的。
/ 源码分析 /
注意点
下文中出现的协程可以理解为协程各自在各自的线程上,且线程不同。其实如果多个协程共用一个线程,其实它们之间也就没有线程并发问题了。
下文中多次出现的loop()方法的源码,方便理解。
public inline fun <T> AtomicRef<T>.loop(action: (T) -> Unit): Nothing {
while (true) {
action(value)
}
}
让我们从扩展方法withLock入手。
public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T {
lock(owner)
try {
return action()
} finally {
unlock(owner)
}
}
这段代码看上去和ReentrantLock使用方式非常类似,先获取锁,获取取到锁之后就可以执行需要保证线程安全的代码块了。执行结束之后释放掉持有的锁。那这里就简单了,重点应该就是这个lock()方法了, ReentrantLock中在线程执行lock()方法获取不到锁之后就会重试等操作,最终还是获取不到锁后线程会加入队列进入自旋状态或者被阻塞,这里就是为什么ReentrantLock是阻塞式的。那我们看下Mutex的lock()都做了什么。
public override suspend fun lock(owner: Any?) {
// fast-path -- try lock
if (tryLock(owner)) return //1
// slow-path -- suspend
return lockSuspend(owner) //2
}
第一步,先尝试获取锁,如果获取到了执行结束,否则执行第二步。
第二步,从字面含义上看是锁住suspend方法,具体方式后面会介绍。
@SharedImmutable
private val LOCKED = Symbol("LOCKED")
@SharedImmutable
private val UNLOCKED = Symbol("UNLOCKED")
@SharedImmutable
private val EMPTY_LOCKED = Empty(LOCKED)
@SharedImmutable
private val EMPTY_UNLOCKED = Empty(UNLOCKED)
private class Empty(
@JvmField val locked: Any
) {
override fun toString(): String = "Empty[$locked]"
}
private val _state = atomic<Any?>(if (locked) EMPTY_LOCKED else EMPTY_UNLOCKED) // 1
public override fun tryLock(owner: Any?): Boolean {
_state.loop { state ->
when (state) {
is Empty -> {
if (state.locked !== UNLOCKED) return false
val update = if (owner == null) EMPTY_LOCKED else Empty(
owner
)
if (_state.compareAndSet(state, update)) return true // 2
}
is LockedQueue -> {
check(state.owner !== owner) { "Already locked by $owner" } // 3
return false
}
is OpDescriptor -> state.perform(this) // 4
else -> error("Illegal state $state")
}
}
}
上述代码都是Mutex的实现类MutexImpl内部代码。
内部持有的状态变量,类型为AtomicRef,这样后续对它的更改都是原子化的,保证线程安全。
第一次进入时,_state还是类型Empty ,更具代码逻辑会走到CAS操作的位置,下一次别的线程执行到这里的时候tryLock()会返回false,进入lockSuspend()方法。
如果已经有线程执行到lockSuspend() ,那么_state会变成类型LockedQueue,说明有协程在等待了,也会返回false,进入lockSuspend()方法。
从源码可以知道,如果现在是OpDescriptor那么现在有协程正在执行unlock()方法,协程正在从LockedQueue出队列,继续loop。
private suspend fun lockSuspend(owner: Any?) = suspendAtomicCancellableCoroutineReusable<Unit> sc@ { cont ->
val waiter = LockCont(owner, cont)
_state.loop { state ->
when (state) {
is Empty -> {
if (state.locked !== UNLOCKED) { // try upgrade to queue & retry
_state.compareAndSet(state, LockedQueue(state.locked))
} else {
// try lock
val update = if (owner == null) EMPTY_LOCKED else Empty(owner)
if (_state.compareAndSet(state, update)) { // locked
cont.resume(Unit)
return@sc
}
}
}
is LockedQueue -> {
val curOwner = state.owner
check(curOwner !== owner) { "Already locked by $owner" }
if (state.addLastIf(waiter) { _state.value === state }) {
// added to waiter list!
cont.removeOnCancellation(waiter)
return@sc
}
}
is OpDescriptor -> state.perform(this) // help
else -> error("Illegal state $state")
}
}
}
// 队列的结构和队列中每个node的结构
private class LockedQueue(
@JvmField var owner: Any
) : LockFreeLinkedListHead() {
override fun toString(): String = "LockedQueue[$owner]"
}
private abstract class LockWaiter(
@JvmField val owner: Any?
) : LockFreeLinkedListNode(), DisposableHandle {
final override fun dispose() { remove() }
abstract fun tryResumeLockWaiter(): Any?
abstract fun completeResumeLockWaiter(token: Any)
}
private class LockCont(
owner: Any?,
@JvmField val cont: CancellableContinuation<Unit>
) : LockWaiter(owner) {
override fun tryResumeLockWaiter() = cont.tryResume(Unit)
override fun completeResumeLockWaiter(token: Any) = cont.completeResume(token)
override fun toString(): String = "LockCont[$owner, $cont]"
}
看到suspend就知道了,这是协程的挂起函数,协程运行到这就挂起了。协程所在的线程就去做别的工作了或者被回收了。
Empty情况下,如果锁已经被持有了,那么修改_state为LockedQueue。如果未被其他协程持有锁,那么当前协程持有锁,并 resume 恢复协程运行。
LockedQueue 情况下,说明肯定又协程持有锁了,加入队列排队。
OpDescriptor同tryLock( 不赘述,继续loop。
上面已经分析完了获取锁的过程,下面介绍释放锁的原理。
public override fun unlock(owner: Any?) {
_state.loop { state ->
when (state) {
is Empty -> {
if (owner == null)
check(state.locked !== UNLOCKED) { "Mutex is not locked" }
else
check(state.locked === owner) { "Mutex is locked by ${state.locked} but expected $owner" }
if (_state.compareAndSet(state, EMPTY_UNLOCKED)) return
}
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()
if (waiter == null) {
val op = UnlockOp(state)
if (_state.compareAndSet(state, op) && op.perform(this) == null) return
} else {
val token = (waiter as LockWaiter).tryResumeLockWaiter()
if (token != null) {
state.owner = waiter.owner ?: LOCKED
waiter.completeResumeLockWaiter(token)
return
}
}
}
else -> error("Illegal state $state")
}
}
}
Empty情况下,修改_state为UNLOCKED即可
OpDescriptor同tryLock()不赘述,继续loop
LockedQueue情况下,获取头节点,头节点不为空,那么从节点中获取持有的 CancellableContinuation,恢复协程。如果为空,其实说明队列中已经没有协程在排队了,执行解锁操作, _state CAS为 OpDescriptor,这个操作其实很简单就是如果队列确实为空,就恢复成_state为 Empty,如果不为空那么修改成 _state为LockedQueue(这部分代码脱离主逻辑,细节可以自行查看,这里不做展开)
/ 总结 /
Mutex大致逻辑还是非常清晰的,协程先获取锁,然后执行代码块,然后释放锁,其他协程如果进入,必须先获取锁,获取不到协程执行挂起方法suspend fun lockSuspend(owner: Any?), 加入等待队列,挂起协程。等待其他协程释放锁之后,恢复协程。
整体还是建立在CAS基础上,封装的一套解决方案,逻辑上非常类似Java的AQS,不过理解难度上会容易很多。
推荐阅读:
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注