【问题标题】:What's the best way to distribute a long running task over multiple frames?将长时间运行的任务分配到多个帧的最佳方法是什么?
【发布时间】:2018-02-06 11:37:06
【问题描述】:

我有多个长时间运行的任务,例如超过约 10 毫秒,这会影响浏览器的响应能力。最糟糕的,例如从文件中加载和解析 3D 模型,已经被卸载到 Web Worker 上,这样它们就不会影响渲染循环。

然而,有些任务不容易移植到 Workers,因此必须分布在主线程中的多个帧上。我不想一次性完成 1 秒的任务,而是将其拆分为约 5 毫秒的包,以使浏览器有机会在其间执行其他事件(鼠标移动、requestAnimationFrame、...)。

生成器函数与 setTimeout 相结合,似乎是最简单的方法。我已经一起破解了一些可以完成这项工作的东西,但我想知道是否有更好/更清洁的方法来解决这个问题。

下面的代码计算 1 亿次 Math.random() 调用的平均值。 第一个版本一次性计算平均值,但会使浏览器停止约 1.3 秒。 第二个版本滥用生成器函数在每 500 万个点后产生,从而使浏览器有机会在其间执行其他事件(鼠标移动)。生成器函数通过 setTimout 循环重复调用,直到处理完所有 1 亿个样本。

<html>
<head></head>
<body>

<script>

    let samples = 100 * 1000 * 1000;

    { // run complete task at once, possibly stalling the browser
        function run(){
            let start = performance.now();

            let sum = 0.0;
            for(let i = 0; i < samples; i++){
                sum = sum + Math.random();
            }

            let mean = sum / samples;

            let duration = performance.now() - start;

            console.log(`single-run: duration: ${duration}`);
            console.log(`single-run: sum: ${sum}`);
            console.log(`single-run: mean: ${mean}`);
        }
        run();
    }


    { // run in smaller packages to keep browser responsive

        // move mouse to check if this callback is executed in between
        document.body.addEventListener("mousemove", () => {
            console.log("mouse moved");
        });

        function * distributedRun(){
            let start = performance.now();

            let packageSize = 5 * 1000 * 1000;
            let sum = 0.0;
            for(let i = 0; i < samples; i++){
                sum = sum + Math.random();

                if((i % packageSize) === 0){
                    yield sum;
                }
            }

            let mean = sum / samples;

            let duration = performance.now() - start;


            console.log(`distributed-run: duration: ${duration}`);
            console.log(`distributed-run: sum: ${sum}`);
            console.log(`distributed-run: mean: ${mean}`);

            yield sum;
        }

        let generatorInstance = distributedRun();
        function loop(){
            let result = generatorInstance.next();
            console.log(`distributed-run intermediate result: ${result.value}`);

            if(!result.done){
                setTimeout(loop, 0);
            }
        }
        loop();
    }

</script>

</body>
</html>

ES2018 有异步迭代器,听起来像我正在寻找的东西,但我不确定它们是否真的适用于这类问题。像这样使用它仍然会停止浏览器:

for await (const result of distributedRun()) {
    ...
}

(在这里和那里以及在 runDistributed() 函数中尝试了一些异步,但暂时,我仍在学习等待/异步的细节)

【问题讨论】:

  • 如果你要阅读异步文档,等待会阻塞当前的执行范围,即如果你使用异步函数,它将返回一个承诺,调用异步函数不会阻塞任何其他事情。然而,据我理解这个问题并能想到,我认为你应该使用基于事件的触发器,我们将在某个时间间隔触发它,监听器将执行剩余的下一个任务块。使用“for await”将停止当前范围,因此它看起来像阻塞浏览器。
  • 不清楚您想如何使用异步迭代器来表示您的任务以及您尝试的方法不起作用。

标签: javascript


【解决方案1】:

这是您的代码稍作修改的版本。如果您根据您的计算复杂性和您可以允许的延迟量调整chunk,它应该可以正常工作。

let samples = 100 * 1000 * 1000;
let chunk = 100000;

async function run() {
    let sum = 0.0;
    for(let i=0; i<samples; i++) {
        sum += Math.random();

        if (i % chunk === 0) {
            console.log("finished chunk")
            // wait for the next tick
            await new Promise(res => setTimeout(res, 0));
        }
    }

    let mean = sum / samples;
    console.log("finished computation", mean);
}

setTimeout(run, 0);

【讨论】:

  • 为什么setTimeout(run, 0),直接调用run()可以得到同样的效果
  • 自从我使用 JavaScript 以来已经有一段时间了,但这与事件循环有关。基本上(据我所知),脚本会运行,然后开始处理超时等内容。因此,如果setTimeout(run, 0) 之后有代码,它将在run() 的第一次迭代之前运行。[Demo] 在这种情况下,这并不重要,但我觉得这是更好的做法。
  • 想象一下,在页面响应之前需要运行大量其他内容,您不希望延迟,以便第一次迭代提前运行。
猜你喜欢
  • 2015-10-01
  • 2015-12-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-27
  • 1970-01-01
相关资源
最近更新 更多