【问题标题】:Kotlin Coroutines lockup/freezingKotlin 协程锁定/冻结
【发布时间】:2021-10-18 05:51:24
【问题描述】:

我在运行协程时遇到奇怪的锁定(冻结)。有时它工作得很好,有时它会等待并在很长一段时间后继续运行(或根本不运行)。我不知道如果我的设置不正确,存在一些竞争条件,或者我误解了应该如何使用协程。应用程序本身不会冻结或崩溃,它只是协程停止/暂停执行。

例如,在这个位中,scope.launch 有时不会继续执行到billingClientDeferred.await()

    override suspend fun getPurchases(): Result<List<Purchase>> = suspendCoroutine { continuation ->
    scope.launch {
        billingClientDeferred.await().success { client ->
            client.queryPurchasesAsync(BillingClient.SkuType.SUBS) Subs@{ billingResultSubs, purchasesSubs ->
                if (billingResultSubs.responseCode != BillingClient.BillingResponseCode.OK) {
                    continuation.resume(Result.Failure.Unknown())
                    return@Subs
                }
                continuation.resume(Result.Success(purchasesSubs))
            }
        }
    }
}

scope 声明为:

private val scope = CoroutineScope(Job() + Dispatchers.Default)

然后从WalletManager调用

override suspend fun verifyProducts(): Result<Unit> = billingProvider.getPurchases()
        .successSuspend { purchases ->
            purchases.forEach {
                activatePurchase(it)
            }
        }.map { }

到底是从ViewModel这样调用的

override fun verify() {
    viewModelScope.launch {
        userSupervisor.walletManager.mapResult {
            it.verifyProducts()
        }
    }
}

我猜我正在使用的调度程序和范围的组合存在一些问题。我尝试了不同的事情,Dispatchers.DefaultDispatchers.Main,符合CoroutineScope 的类,使用全局范围,但我总是遇到一些我不完全理解的线程/锁定问题。

【问题讨论】:

  • 你在suspendCoroutine里面启动一个新协程的具体原因是什么,你清楚地理解suspendCoroutine函数的用例吗?
  • 因为我在内部使用了基于回调的函数client.queryPurchasesAsync 和基于协程的billingClientDeferred.await()。我可以提取queryPurchasesAsync 将其转换为挂起函数,然后就不需要在getPurchases 中使用scope.launch。这样做是不好的做法吗?这可能是我的问题的原因吗?
  • 这是一种不好的做法,因为您的协程在父子关系中不再相关。您使用 viewModelScope 触发了您的根协同程序,并且在该协同程序中您使用您定义的范围触发了另一个协同程序。除非您自己取消该范围,否则当取消 viewModelScope 时,它​​不会取消使用您的范围触发的协程。 suspendCoroutine 正在桥接基于回调的代码和基于协程的代码。转换需要转换的任何内容以避免在suspendCoroutine中触发新的协程。

标签: android kotlin kotlin-coroutines coroutine


【解决方案1】:

第一个问题是你不想从suspendCoroutine 中启动一个新的协程,你需要重组你的代码,这样你就不必这样做了。这可以这样做

private fun someMethod() = viewModelScope.launch {
    val client = billingClientDeferred.await()
    val finalResult = getPurchases(client)
}

在此之后只需将getPurchases 更新为除了客户端作为参数并从中删除所有协程代码,只需调用客户端并使用延续返回结果。

您的代码的根本问题是您没有遵循structured concurrency 的原则。

它本质上是一种并发管理模式,隐含地处理协程的范围和生命周期。这个想法是,当您使用不同的范围启动协程时,这些范围必须具有父子关系,这允许协程管理系统处理所有情况,例如

  1. 当内部协程抛出异常时会发生什么
  2. 当外部协程被取消或失败时会发生什么

恐怕您的代码并没有真正遵循这种模式并且存在以下问题

scope 与启动 getPurchasesCoroutineScope 没有任何关系

这是个大问题,如果外部作用域被取消了,是取消内部协程还是内部协程继续消耗资源?

您正在从挂起函数启动协程

正如 Roman Elizarov 解释的 here

另一方面,挂起函数被设计为 非阻塞,不应该有启动任何副作用 并发工作。暂停功能可以而且应该等待所有 在返回调用者之前完成他们的工作。

【讨论】:

  • 谢谢你的回答,我想我现在明白了。简而言之,launch 中不应该有 launch
  • async 怎么样?我猜这很好,因为它通过await 连接到外部协程,所以不会发生同样的问题。使用这样的附加范围是否正确? private val billingClientDeferred = scope.async(start = CoroutineStart.LAZY) { billingClient() }
  • @Rafal 我建议您仔细阅读协程文档。可以在其他协程内部触发协程,但嵌套协程应该是外部协程的子协程。这是默认行为。它可以通过以下任一方式破坏:a)为嵌套协同程序提供另一个 Job 实例 b)使用与外部协同程序范围不同的范围启动嵌套协同程序(这就是您所做的,外部协同程序使用 viewModelScope 触发,嵌套协同程序使用您定义的范围)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-03
  • 2021-08-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多