后台
我在我工作的一个大型应用程序上发现了一些类/函数,它们的调用不应该在UI线程上(比如访问存储或DB)。
这样的操作可能会导致ANR,实际上我可以在Play控制台上看到ANR的百分比。
我想改变这一点,并希望通过使用Kotlin协程也有更多的代码顺序。
因此,目前我正在处理一个扩展BroadcastReceiver
的类,因此它需要在UI线程上一个接一个地处理onReceive
回调,每个回调都必须“等待”前一个回调完成。
在onReceive回调中,有些调用应该在后台线程上完成,有些应该在UI线程上完成。有时候有些情况下两者都有。
例如:
if( someCheckOnUiThread() && someDbOperation()) {
...
}
问题
我是Kotlin协程的新手,尽管我已经找到了如何处理这个问题的方法,但我很确定有一种更官方的方法来处理这个问题,因为我已经从其他人那里读到了一些提示和评论(here)。
我尝试过的
我所做的实际上是有效的,但它看起来更像是一个解决方案:
private val mainScope = MainScope()
private val backgroundWorkDispatcher: CoroutineDispatcher =
java.util.concurrent.Executors.newFixedThreadPool(1).asCoroutineDispatcher()
然后在onReceive
回调中使用它们:
@UiThread
override fun onReceive(somcContext: Context, intent: Intent) {
val context = somcContext.applicationContext
//use goAsync just because I'm in BroadcastReceiver
val pendingAsyncResult = goAsync()
mainScope.launch {
runInterruptible(backgroundWorkDispatcher) {
// <-- some code here
}
}.invokeOnCompletion { throwable ->
// last operation after done with everything here:
pendingAsyncResult.finish()
}
//done right away here, and yet the handling will be done one after another, freely
}
在runInterruptible
内部,我可以通过调用runBlocking(mainScope.coroutineContext) {}
来访问UI线程,甚至在内部也可以使用cancel()
取消任务。
使用runBlocking
很重要,因为我需要在那里等待结果。当然,当有意义的时候,我可以使用替代方案,但是我也可以使用一个简单的处理程序,因为我不等待结果。
我还使用backgroundWorkDispatcher
来确保所有的后台操作都在一个线程上,以等待下一个操作,一个接一个。
问题
这种解决方案的替代方案是什么?更优雅和/或更短的?更正式的?
注意,我需要处理UI一个接一个排队的操作,每个操作都等待前一个操作完成。BroadcastReceiver
只是一个例子。我确信在代码中有(可悲的)更难修复的地方,但我想首先知道如何正确处理这个问题。
2条答案
按热度按时间unhi4e5o1#
由于您在对另一个问题的评论中询问了线程队列,下面是我如何做协程作业队列。请记住,这是如果您需要每个提交的协程完全按顺序运行(根本没有并行工作),我不确定您上面所描述的是什么。
您可以在
object
中或在顶层创建此类的示例,以使其持续应用的生命周期。这取决于您需要作业运行多长时间。我没有太多的BroadcastReceiver经验,但我知道它们是短暂的,所以如果他们在你的应用程序关闭屏幕时收到了一些东西,而协同程序需要的时间超过几秒钟,我不确定到底会发生什么。对于这类工作,我认为你需要迅速把它交给一个工作经理。但是如果你在应用程序显示在屏幕上的时候做一些事情,你可以使用协程。以下操作将防止已提交作业的任何部分在任何以前提交到同一JobQueue示例的作业之前运行。
关于问题中的代码:
创建单线程调度器可以防止使用该调度器的代码并行运行,而这可能正是您所希望的。但它不创建队列,也不保证执行顺序。假设我上面的例子是用你的解决方案完成的。操作系统快速连续地对
onReceive
进行了两次调用。doSomeBlockingFetch()
部分不会使用单线程调度器并行运行,但不能保证它们将以何种顺序被调用,也不能保证随后的doSomethingWithResult()
将以何种顺序被调用。如果你想要一种不那么笨拙的方法来防止你的阻塞代码并行运行,并且如果你不关心IO后主线程工作的执行顺序,我会使用互斥体而不是单线程调度器:
下面是一个带有热SharedFlow的Job Queue类的示例,因为您需要它,但这将是一个奇怪的选择。当已经有Channels时,SharedFlow被添加到Kotlin的全部原因是为多个订阅者提供一种无需消费就可以获取值的方法,而不是每个值只允许消费一次,无论谁在阅读它。但是对于一个作业队列,我们不希望有多个订阅者,我们只希望每个作业使用一次。因此,如果您使用SharedFlow执行此操作,就像使用扳手敲钉子一样。它会起作用,但不像用锤子那么优雅,而且有更多的误用或事故的风险。
x759pob22#
我还使用backgroundWorkDispatcher来确保所有后台操作都在单个线程上,以等待下一个操作,一个接一个。
这不是协程世界中单个线程所强制执行的。单个线程阻止了 * 并行性 *,但并不阻止 * 并发性 *。如果在单线程调度器中执行
launch
2个协程,那么第二个协程很可能在第一个协程结束之前开始,假设第一个协程至少有一个挂起点(调用挂起函数)。这种协同程序执行的交错就是并发的含义。参见this other question,它源于相同的误解。基本上,使用
launch
或async
协程构建器(或者更一般地,启动多个协程)表示这些代码片段之间的并发性。虽然可以设计一个自定义分派器,强制一个协程在下一个协程开始之前完成,但这与协程的预期相反。现在,这并不意味着你不能在你的情况下做任何事情,它只是意味着每个事件启动一个协程可能不是正确的事情(也许这是好的,虽然,请参阅下面的编辑)。相反,我建议创建一个channel来表示要处理的事件队列,并在某个地方启动一个协程来处理这些事件(因此它正确地表示您不需要并发)。在
onReceive
中,每个事件不启动一个协程,而只是发送到通道(在您的情况下可能使用sendBlocking
,因为onReceive
不是suspend
)。现在,关于在哪里启动“演员”协程,我想说这取决于。您可以为通道和协程指定您想要的作用域(我的意思是“作用域”是指变量可见性范围,而不是协程)。例如,如果你只想在这个特定的
BroadcastReceiver
的事件之间强制非并发性,我会说将通道声明为这个广播接收器的属性,并在初始化时启动协程(例如:在init
块中),该CoroutineScope
的作用域为广播接收器的生命周期(如果没有lifecycleScope
,请自己创建作用域,并在销毁BroadcastReceiver
时取消它)。在onReceive回调中,有些调用应该在后台线程上完成,有些应该在UI线程上完成。有时候有些情况下两者都有。
这不是一个大问题。协程通过使用
withContext(someDispatcher)
使线程之间的切换变得容易。请注意,withContext
在返回之前会等待lambda中的任何代码。这意味着即使在线程之间切换,代码仍然是顺序的。同步已为您处理。下面是一些关于它外观的示例代码:
编辑:从
goAsync
的文档中,我意识到使事件处理异步实际上会阻止接收其他事件,我假设这会阻止对onReceive
的并发调用:请记住,您在这里所做的工作将阻止进一步的广播,直到它完成
这意味着您实际上可以启动任意多个协程来处理这些事件,只要您在处理结束时完成
goAsync
未决结果即可。但是不需要runBlocking
/runInterruptible
,只需要在这里和那里使用withContext
就可以了。