android - 为什么 Kotlin Coroutine withTimeoutOrNull 花费的时间比分配的时间长?
问题描述
我正在开发一个视频编辑工具,我有一个渲染场景的功能。根据该框架的复杂性,这可能需要一段时间才能发生。为了使视频编辑器顺利运行,我必须分配一定的时间来渲染每一帧。在这段时间内,如果帧被渲染,我们将其显示给用户,如果没有,则跳过该帧并继续下一帧。问题是当我运行一个挂起函数并要求协程为其分配一定的毫秒数时,实际上需要更长的时间才能发生。此处包含的实际代码非常复杂,但是我制作了一个代理代码,并且也遇到了同样的问题。这是我的代理:
我有一个可能需要很长时间的渲染功能。所以,我给它的最大时间(延迟)为 10 秒:
suspend fun MainActivity.render()
{
// in each frame, we print the time (in seconds) to the screen
// frame rate is 25
findViewById<TextView>(R.id.testTextView).text =
String.format("%.2f", currentFrame.toFloat() / 25f)
delay(10000)
}
现在,我有我的播放函数,它在每一帧中调用一次渲染。在这个例子中,我假设每一秒由 25 帧组成。所以每一帧的长度为 40 毫秒 (1000 / 25)。所以,播放的代码如下。在这段代码中,每一帧要么在 40 毫秒内被渲染,要么我们应该进入下一个周期:
suspend fun MainActivity.play()
{
val elapsedTime = measureTimeMillis {
// I assume the total is 200 frames or 8 seconds.
while (currentFrame < 200)
{
withTimeoutOrNull(40) // make or break in 40 millies
{
withContext(Dispatchers.Main)
{
currentFrame += 1
render()
}
}
}
}
findViewById<TextView>(R.id.totalTextView).text =
String.format("It took: %.2f", elapsedTime.toFloat() / 1000f)
}
最后,我在一个按钮中调用了这个函数:
class MainActivity : AppCompatActivity()
{
var currentFrame = 0
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.testButton).setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
play()
}
}
}
}
你可以查看这张图片,看看当我运行这段代码时,本来应该在 8 秒内发生的事情,实际上需要 8.37 秒。有关修复此代码或通常达到相同结果的任何建议?
解决方案
When thinking about cancelling some background coroutines/threads, we need to understand one thing: it is technically impossible (?) to stop/cancel/interrupt a running code. That means cancellations are always cooperative. This is the same for cancelling a coroutine in Kotlin and for interrupting a background thread with Thread.interrupt()
- code running in the interrupted thread need to intentionally check if it was interrupted, otherwise it will be still running normally.
We can see this clearly by executing this code:
val time = measureTimeMillis {
runBlocking {
val job = async {
Thread.sleep(3000)
}
delay(1000)
job.cancel()
}
}
println("time: $time")
Despite the fact our async task was cancelled after 1000ms, it is running for a full 3000ms. It was cancelled, but it can't stop working, because it is doing something (sleeping).
Now, change this code to:
val time = measureTimeMillis {
runBlocking {
val job = async {
repeat(6) {
Thread.sleep(500)
yield()
}
}
delay(1000)
job.cancel()
}
}
println("time: $time")
This time it takes about 1.5s - after being cancelled, it finishes in the next yield()
window.
To make your code more responsive to cancelling, you need to regularly check if a coroutine is still active or just call a suspending function that does this (yield()
is one option). Also, you should not really assume you can control timings with such precision. It will always take a little more time than you expected.
You can read more about this topic here
推荐阅读
- laravel - Laravel:$errors->first() 不显示错误。如何让它发挥作用?
- automation - 正则表达式 (0+1)*1(0+1)*0 DFA
- css - 如何将 CSS 类添加到 Flask-Table 列
- xml - 增量元素名称的 xml 架构列表
- android - 如何在运行时在 mapbox 中添加具有不同图像的标记?
- google-analytics - 属性设置未在 Google Analytics 中加载
- java - Spring中“排序超出内存限制”MongoDb的问题
- solr - Apache Solr 原子更新导致 NullPointerException
- angular - 离子如何将离子范围的数据存储到firebase数据库中
- windows - 需要静默提供 Windows 批处理文件和 shell 脚本两个用户输入