Kotlin协程中的并发问题解决方案


/   今日科技快讯   /

9月1日,雷军社交媒体晒图:小米汽车新同学,今天开学。并公布了小米汽车公司名:小米汽车有限公司。雷军表示,小米汽车有限公司注册资金100亿,雷军担任法人代表。

/   作者简介   /

本篇文章来自ahhsfj1991的投稿,文章主要分享了kotlin开发中通过Mutex解决多并发问题,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

/   问题背景   /

如果我们同时开启多条线程去修改某一变量时,最终的结果可能不是我们所预期的。如下方的代码所示:

fun main(args: Array<String>) {
    var count = 0
    (0..1000).forEach { _ ->
        Thread(Runnable {
            count++
        }).start()
    }
    println(count)
}

相信这段代码大家基本都看到过,这里不再介绍为什么会出现并发问题,解决方案也很多,在以前Java中大致有三个思路:

  1. synchronized 

  2. AtomicInteger

  3. 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内部代码。  

  1. 内部持有的状态变量,类型为AtomicRef,这样后续对它的更改都是原子化的,保证线程安全。

  2. 第一次进入时,_state还是类型Empty ,更具代码逻辑会走到CAS操作的位置,下一次别的线程执行到这里的时候tryLock()会返回false,进入lockSuspend()方法。  

  3. 如果已经有线程执行到lockSuspend() ,那么_state会变成类型LockedQueue,说明有协程在等待了,也会返回false,进入lockSuspend()方法。

  4. 从源码可以知道,如果现在是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就知道了,这是协程的挂起函数,协程运行到这就挂起了。协程所在的线程就去做别的工作了或者被回收了。

  1. Empty情况下,如果锁已经被持有了,那么修改_state为LockedQueue。如果未被其他协程持有锁,那么当前协程持有锁,并 resume 恢复协程运行。

  2. LockedQueue 情况下,说明肯定又协程持有锁了,加入队列排队。

  3. 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")
        }
    }
}
  1. Empty情况下,修改_state为UNLOCKED即可

  2. OpDescriptor同tryLock()不赘述,继续loop

  3. LockedQueue情况下,获取头节点,头节点不为空,那么从节点中获取持有的 CancellableContinuation,恢复协程。如果为空,其实说明队列中已经没有协程在排队了,执行解锁操作, _state CAS为 OpDescriptor,这个操作其实很简单就是如果队列确实为空,就恢复成_state为 Empty,如果不为空那么修改成 _state为LockedQueue(这部分代码脱离主逻辑,细节可以自行查看,这里不做展开)

/   总结   /

Mutex大致逻辑还是非常清晰的,协程先获取锁,然后执行代码块,然后释放锁,其他协程如果进入,必须先获取锁,获取不到协程执行挂起方法suspend fun lockSuspend(owner: Any?), 加入等待队列,挂起协程。等待其他协程释放锁之后,恢复协程。

整体还是建立在CAS基础上,封装的一套解决方案,逻辑上非常类似Java的AQS,不过理解难度上会容易很多。

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

抖音传送带特效实战

新版Glance发布,更好用的Android数据库调试助手

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值