首页 > 解决方案 > 为什么 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 秒。有关修复此代码或通常达到相同结果的任何建议?

在此处输入图像描述

标签: androidandroid-studiokotlinconcurrencykotlin-coroutines

解决方案


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


推荐阅读