Kotlin协程的共享状态与并发(十)

一、前言

​ 在多线程开发中,会遇到多个线程修改读取单个值的情况。协程中也是如此

二、有问题的代码

​ 这里启动100个协程,执行1000次同样的操作看看结果如何

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")    
}

var counter = 0

@Test
fun test() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            counter++
        }
    }
    println("Counter = $counter")
}

我们预期的结果是Counter = 100000,实际运行时候会发现结果不太可能是这个。

三、Volatile

​ 使用volitile如何呢

@Volatile // 在 Kotlin 中 `volatile` 是一个注解
var counter = 0

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            counter++
        }
    }
    println("Counter = $counter")
}

会发现运行效果变慢了,但是问题却没有解决。其实本身volitile是不能解决并发问题。它的使用场景是,一个线程写,多个线程读的情况。

四、线程安全的数据结构

​ 如果使用线程安全的数据结构作为数据存储的话,那么问题就可以解决。

val counter = AtomicInteger()

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            counter.incrementAndGet()
        }
    }
    println("Counter = $counter")
}

这也是通常情况下的常规操作。不过对于复杂操作的话,就比较难以处理

五、粒度控制

​ 多线程修改共享状态是因为多个线程来修改同一个变量,如果将线程范围锁定在一个线程中就没有这个问题了

1、细粒度控制

​ 创建一个单线程的协程上下文来约束协程修改的范围

val counterContext = newSingleThreadContext("CounterContext")
var counter = 0

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            // 将每次自增限制在单线程上下文中
            withContext(counterContext) {
                counter++
            }
        }
    }
    println("Counter = $counter")
}

执行后可以发现结果符合我们预期,只是运行很慢,这是因为每次修改值的时候都会发生上下文切换的问题。那么将整个范围扩大就可以解决这个问题

2、粗粒度控制

为了加快运行速度,避免频繁的上下文切换。我们扩大协程的范围

val counterContext = newSingleThreadContext("CounterContext")
var counter = 0

fun main() = runBlocking {
    // 将一切都限制在单线程上下文中
    withContext(counterContext) {
        massiveRun {
            counter++
        }
    }
    println("Counter = $counter")
}

可以看到运行效果变快了而且结果也正确

六、互斥

​ 互斥的意思是:使用永远不会同时执行的 关键代码块 来保护共享状态的所有修改使用。在阻塞的世界中,你通常会为此目的使用 synchronized 或者 ReentrantLock。 在协程中的替代品叫做 Mutex 。它具有 lockunlock 方法, 可以隔离关键的部分。关键的区别在于 Mutex.lock() 是一个挂起函数,它不会阻塞线程。

还有 withLock 扩展函数,可以方便的替代常用的 mutex.lock(); try { …… } finally { mutex.unlock() } 模式:

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")    
}

val mutex = Mutex()
var counter = 0

fun main() = runBlocking {
    withContext(Dispatchers.Default) {
        massiveRun {
            // 用锁保护每次自增
            mutex.withLock {
                counter++
            }
        }
    }
    println("Counter = $counter")
}

此示例中锁是细粒度的,因此会付出一些代价。但是对于某些必须定期修改共享状态的场景,它是一个不错的选择,但是没有自然线程可以限制此状态。

七、Actors

一个 actor 是由协程、 被限制并封装到该协程中的状态以及一个与其它协程通信的 channel 组合而成的一个实体。可以在一些复杂场景下满足需求。

使用 actor 的第一步是定义一个 actor 要处理的消息类。 Kotlin 的密封类很适合这种场景。 我们使用 IncCounter 消息(用来递增计数器)和 GetCounter 消息(用来获取值)来定义 CounterMsg 密封类。 后者需要发送回复。CompletableDeferred 通信原语表示未来可知(可传达)的单个值, 这里被用于此目的。

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")    
}

// 计数器 Actor 的各种类型
sealed class CounterMsg
object IncCounter : CounterMsg() // 递增计数器的单向消息
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // 携带回复的请求

// 这个函数启动一个新的计数器 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)
        }
    }
}

@Test
fun test() = 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 与它发送元素的通道相关联。

八、参考链接

  1. 共享可变状态与并发

    https://www.kotlincn.net/docs/reference/coroutines/shared-mutable-state-and-concurrency.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Kotlin 协程Kotlin Coroutines)提供了一种结构化并发的方式,可以更加方便和自然地管理异步操作和并发任务。它们可以帮助开发者避免使用传统的线程和回调函数的方式,从而提高代码的可读性和可维护性。 以下是 Kotlin 协程实现结构化并发的主要方式: 1. 使用 suspend 关键字标记异步操作的函数 使用协程时,可以将异步操作的函数声明为 suspend 函数。这些函数在执行到异步操作时可以挂起,等待异步操作完成后再继续执行。这样,代码可以更加自然地按照异步操作的顺序执行,并且可以避免回调函数的嵌套。 例如,以下是一个使用 Retrofit 库进行网络请求的示例,其中的网络请求函数使用了 suspend 关键字标记: ```kotlin suspend fun fetchUser(userId: String): User { val response = retrofitService.getUser(userId) return response.body()!! } ``` 2. 使用协程作用域来管理并发任务 协程作用域是一种用于管理协程生命周期的机制。通过使用协程作用域,可以创建一个由多个协程组成的任务,确保这些协程在同一时刻开始和结束,从而实现结构化并发。 例如,以下是一个使用协程作用域启动多个协程执行并发任务的示例: ```kotlin suspend fun fetchUserData(userIds: List<String>): List<User> = coroutineScope { userIds.map { userId -> async { fetchUser(userId) } }.awaitAll() } ``` 在这个示例中,使用了协程作用域 `coroutineScope` 来创建一个由多个协程组成的任务。每个协程都是通过 `async` 函数创建的,并在 `awaitAll` 函数中等待所有协程执行完毕后返回结果。 3. 使用协程的异常处理机制 协程还提供了一种更加自然的异常处理机制。通过使用 `try/catch` 块捕获异常,可以在异步操作出现异常时立即处理它,而不需要在回调函数中处理异常。这可以提高代码的可读性和可维护性。 例如,以下是一个使用 `try/catch` 块处理协程中的异常的示例: ```kotlin suspend fun fetchUserData(userIds: List<String>): List<User> = coroutineScope { try { userIds.map { userId -> async { fetchUser(userId) } }.awaitAll() } catch (e: Exception) { // 处理异常 emptyList() } } ``` 在这个示例中,使用了 `try/catch` 块来捕获协程中的异常,并在发生异常时返回一个空列表。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值