【Kotlin回顾】20.Kotlin协程—协程的并发

874 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第20天,点击查看活动详情

1.协程与并发

Kotlin中协程是运行在线程之上的,它也是可以实现并发的并且也会遇到并发问题

fun concurrentTest() = runBlocking {
    val jobs = mutableListOf<Job>()
    var i = 0

    repeat(10) {
        val job = launch(Dispatchers.Default) {
            repeat(1000) {
                i++
            }
        }
        jobs.add(job)
    }

    jobs.joinAll()

    println("i: $i")
}

//输出结果:有等于10000的,有小于10000的,不稳定

上面的代码创建了10个协程任务,每个协程都会运行在Default的线程上,这10个协程任务分别会对变量i执行1000次自增操作,按照代码逻辑推测执行结果应该是10000,但是最终的执行结果却不一定就是10000,这是因为这10个协程共享一个变量i,并且这10个协程还运行在不同的线程上,每个线程都会对这个变量进行自增。

那要如何解决这个问题?借助Java的方式可以使用synchronized

fun concurrentTest() = runBlocking {
    val jobs = mutableListOf<Job>()
    var i = 0
    val lock = Any()

    repeat(10) {
        val job = launch(Dispatchers.Default) {
            repeat(1000) {
                synchronized(lock) {
                    i++
                }
            }
        }
        jobs.add(job)
    }

    jobs.joinAll()

    println("i: $i")
}

//输出结果:
//: 10000

使用synchronized之后无论运行多少次得到的最终结果都是10000,问题解决,但是会产生一个新的问题。

2.协程的并发——单线程并发

Kotlin协程具有挂起和恢复的能力,且具有非阻塞的特点,因此可以利用这个特点实现单线程并发。例如下面的代码实现:

fun concurrentTest() = runBlocking {
    suspend fun concurrentResult1(): String {
        logX("start concurrentResult1")
        delay(1000L)
        return "concurrentResult1"
    }

    suspend fun concurrentResult2(): String {
        logX("start concurrentResult2")
        delay(1000L)
        return "concurrentResult2"
    }

    suspend fun concurrentResult3(): String {
        logX("start concurrentResult3")
        delay(1000L)
        return "concurrentResult3"
    }

    val list: List<String>

    val time = measureTimeMillis {
        val result1 = async { concurrentResult1() }
        val result2 = async { concurrentResult2() }
        val result3 = async { concurrentResult3() }

        list = listOf(result1.await(), result2.await(), result3.await())
    }

    logX("time: $time")
    logX("list: $list")
}

fun logX(any: Any?) {
    println(
        """
================================
$any 
Thread:${Thread.currentThread().name}
================================
""".trimIndent()
    )
}

//输出结果:
//================================
//start concurrentResult1 
//Thread:main
//================================
//================================
//start concurrentResult2 
//Thread:main
//================================
//================================
//start concurrentResult3 
//Thread:main
//================================
//================================
//time: 1143 
//Thread:main
//================================
//================================
//list: [concurrentResult1, concurrentResult2, concurrentResult3] 
//Thread:main
//================================

可以看到这段代码是并发执行,耗时在1000毫秒左右,如果三个任务时长不一样那么耗时会按照最长的返回,这个在协程启动方式中提到过,另外从输出日志中也可以发现这些任务都云信在主线程中,这也就证明了本节的标题——单线程并发。

Kotlin协程可以实现单线程并发,那么在开发中遇到并发问题就要考虑,当前的并发问题是否真的需要多个线程?如果需要多个线程的话就要考虑多线程之间的同步问题了。

3.协程的并发——Mutex

Kotlin协程的一大优势就是非阻塞式,在Java中存在类似于Lock的同步锁,而同步锁是会造成阻塞的,如果在协程中实现就会对协程的非阻塞造成很大的影响,因此在Kotlin中不推荐直接使用Java 的锁机制,Kotlin提供了另一套非阻塞式的锁——Mutex。 把开篇第一段代码用Mutex实现

fun concurrentTest() = runBlocking {
    val time = measureTimeMillis {
        val jobs = mutableListOf<Job>()
        var i = 0
        //实例化Mutex
        val mutex = Mutex()

        repeat(10) {
            val job = launch(Dispatchers.Default) {
                repeat(1000) {
                    //锁
                    mutex.lock()
                    i++
                    //解锁
                    mutex.unlock()
                }
            }
            jobs.add(job)
        }

        jobs.joinAll()

        println("i: $i")
    }
    println("time: $time")
}

//输出结果:
//i: 10000
//time: 70

从运行结果和运行所耗时间来看Mutex的加入对程序耗时的影响并不大但是缺实现了正确的结果。这主要得益于Mutex的挂起与恢复的能力

public interface Mutex {

    /**
     * 当这个互斥对象被锁定时返回' true '。
     */
    public val isLocked: Boolean

    /**
     * 锁定互斥锁,在锁定互斥锁时暂停调用方。
     * 这个暂停功能是可以取消的。如果当前协程的Job在此函数挂起时被取消或完成,
     * 此函数将立即以CancellationException恢复。我们保证立即取消。
     * 如果在此函数挂起时job被取消,那么它将无法成功恢复。
     */
    public suspend fun lock(owner: Any? = null)
    
    /**
     * 解开这个互斥锁。如果在互斥对象上调用未锁定的互斥对象或被不同的
     * 所有者令牌(通过身份)锁定的互斥对象时,抛出IllegalStateException。
     */
    public fun unlock(owner: Any? = null)
    
    /**
     * 检查所有者锁定的互斥锁
     * 返回:互斥对象被所有者锁定时为true,如果不是locker或被不同所有者锁定则为false
     */
    public fun holdsLock(owner: Any): Boolean
}

可以看到,Mutex 是一个接口,它的 lock() 方法其实是一个挂起函数。而这就是实现非阻塞式同步锁的根本原因。

除了mutex.lockmutex.unlock之外还有一个Mutex的扩展函数withLock,它的作用是在finally{}中调用unlock()。这可以解决因为异常导致unlock无法继续执行的问题非

假设在unlock之前无造成了一个错误并且捕获了:

fun concurrentTest() = runBlocking {
    val time = measureTimeMillis {
        val jobs = mutableListOf<Job>()
        var i = 0
        val mutex = Mutex()

        repeat(10) {
            val job = launch(Dispatchers.Default) {
                repeat(1000) {
                    try {
                        mutex.lock()
                        i++
                        i / 0				//这里报错
                        mutex.unlock()
                    } catch (e: Exception) {
                        e.printStackTrace()
                    }
                }
            }
            jobs.add(job)
        }

        jobs.joinAll()

        println("i: $i")
    }
    println("time: $time")
}

上面的报错因为被捕获了因此程序无法继续执行也不能结束,想要结束就只能加入finally{},并添加退出程序的代码,withLock的引入则可以解决这个问题:

fun concurrentTest() = runBlocking {
    val time = measureTimeMillis {
        val jobs = mutableListOf<Job>()
        var i = 0
        val mutex = Mutex()

        repeat(10) {
            val job = launch(Dispatchers.Default) {
                repeat(1000) {
                    mutex.withLock {	//withLock替代lock()和unlock()
                        i++
                        i / 0
                    }
                }
            }
            jobs.add(job)
        }

        jobs.joinAll()

        println("i: $i")
    }
    println("time: $time")
}
//withLock源码
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)
    }
}

可以看到withLock源码中finally{}调用了unlock(),这就解决了因为异常导致unlock()不能执行的问题。

4.协程的并发——Actor

Actor,是一个并发同步模型,在Kotlin它的本质是基于Channel管道消息实现的。下面先来看一个例子:

sealed class Msg
object AddMsg : Msg()
class ResultMsg(val result: CompletableDeferred<Int>) : Msg()

fun main() = runBlocking {
    suspend fun addActor() = actor<Msg> {
        var counter = 0
        for (msg in channel) {
            when (msg) {
                is AddMsg -> counter++
                is ResultMsg -> msg.result.complete(counter)
            }
        }
    }

    val actor = addActor()
    val jobs = mutableListOf<Job>()

    repeat(10) {
        val job = launch(Dispatchers.Default) {
            repeat(1000) {
                actor.send(AddMsg)
            }
        }
        jobs.add(job)
    }

    jobs.joinAll()

    val deferred = CompletableDeferred<Int>()
    actor.send(ResultMsg(deferred))

    val result = deferred.await()
    actor.close()

    println("i = $result")
}

//输出结果:
//i = 10000

这里定义了一个挂起函数addActor,具体来讲它调用的是actor()这个高阶函数。这个高阶函数返回的是SendChannel。所以Kotlin中的Actor其实就是Channel的简单封装,Actor的多线程同步能力都来源于Channel。

这段代码中首先在actor{}内部处理定义的两种消息类型,如果收到AddMsg则计算counter,如果收到ResultMsg则返回计算结果,在actor{}外部发送了10000次AddMsg消息,最后再发送一次ResultMsg(deferred)获取最终结果。

代码其实比较好理解的,这里的actor.send(AddMsg)actor.send(ResultMsg(deferred))其实就是SendChannel.send()的执行,所以就确认了Actor的本质是基于Channel管道消息实现的。

5.协程的并发——避免共享可变状态

在第一段代码中出现了程序运行结果不能达到正确结果的主要原因就是10个协程中共享了一个变量“i”,有了这个共享可变状态的时候就需要考虑同步问题,而这个状态是可以改变的:

fun concurrentTest() = runBlocking {
    val deffereds = mutableListOf<Deferred<Int>>()

    repeat(10) {
        var i = 0
        val deferred = async {
            repeat(1000) {
                i++
            }
            return@async i
        }
        deffereds.add(deferred)
    }

    var result = 0
    deffereds.forEach {
        result += it.await()
    }

    println("result: $result")
}

这里通过在协程内部定义变量,并将返回的defered添加到defereds中,待任务执行完毕后再通过遍历将结果累加起来就可以得到正确的结果。