0

我正在尝试与主线程异步运行一段 JavaScript 代码。我不一定需要代码在不同的线程上实际运行(因此性能不需要比顺序执行更好),但我希望代码与主线程并行执行,这意味着不会冻结。

此外,所有需要的代码都需要包含在一个函数中。

我的示例工作负载如下:

function work() {
    for(let i=0; i<100000; i++)
        console.log("Async");
}

另外,我可能在主线程上有一些工作(允许冻结一边,只是为了测试):

function seqWork() {
    for(let i=0; i<100000; i++)
        console.log("Sequential");
}

预期的输出应该是这样的:

Sequential
Async
Sequential
Sequential
Async
Sequential
Async
Async
...

你明白了。

免责声明:我在 JavaScript 和async使用await.

我试过的

我做了一些研究,发现了这三个选项:

1. async/await

似乎是显而易见的选择。所以我尝试了这个:

let f= async function f() {
    await work();
}
f();
seqWork();

输出:

Async (100000)
Sequential (100000)

我也试过:

let f = async function f() {
    let g = () => new Promise((res,rej) => {
        work();
        res();
    });
    await g();
}
f();
seqWork();

输出:

Async (100000)
Sequential (100000)

所以这两种方法都不起作用。他们还在异步输出期间冻结了浏览器,所以这似乎完全没有效果(?)我可能在这里做错了什么,但我不知道是什么。

2. Promise.all

这似乎被称赞为任何昂贵任务的解决方案,但如果您有许多阻塞任务并且您想将它们“组合”成一个比顺序执行更快的阻塞任务,这似乎是一个合理的选择。这当然有用例,但对于我的任务来说它是无用的,因为我只有一个任务要异步执行,并且主“线程”应该在该任务期间继续运行。

3. 工人

在我看来,这似乎是最有希望的选择,但我还没有让它发挥作用。主要问题是您似乎需要第二个脚本。我不能这样做,但即使在使用第二个文件进行本地测试时,Firefox 也会阻止加载该脚本。


这是我尝试过的,我在研究中没有找到任何其他选择。我开始认为这样的事情在 JS 中是不可能的,但这似乎是一个非常简单的任务。同样,我不需要实际并行执行它,如果事件循环在从主线程调用语句和异步“线程”之间交替调用就足够了。来自 Java,它们还能够在单个硬件线程上模拟多线程。


编辑:上下文

我有一些使用 TeaVM 转换为 JavaScript(我无法控制转换)的 java 代码。Java 本身就支持多线程,我的很多代码都依赖于这种可能性。现在由于 JavaScript 显然并不真正支持真正的多线程,TeaVM 以最简单的方式将 Thread 转换为 JS:Thread.start()直接调用Thread.run()使得它完全无法使用。我想在这里创建一个更好的多线程仿真,它几乎可以在不修改的情况下执行线程代码。现在它并不理想,但可以在 java 代码中插入“yielding”语句。

TeaVM 有一个方便的功能,它允许您编写带有匹配 JS 代码注释的本地 Java 方法,这些代码将直接转换为该代码。问题是,您无法设置方法主体,因此您无法将其设为异步方法。

我现在尝试做的一件事是实现一个 JS 原生“yield”/“pause”(不使用 JS 中的关键字)函数,我可以调用该函数以允许其他代码直接从 java 方法运行。该方法基本上必须暂时阻止调用代码的执行,而是调用其他排队任务的执行。我不确定主代码不在异步函数中是否可行。(我无法更改生成的 JS 代码)

我能想到的解决此问题的唯一另一种方法是让 JS 方法调用所有阻塞代码,并参考 Java 代码。但主要问题是,这意味着将 java 方法的方法体拆分为许多小块,因为 Java 不支持yield returnC# 之类的东西。这基本上意味着对每一个并行执行的代码进行彻底的返工,我会拼命避免这种情况。此外,您不能从被调用的方法中“屈服”,从而使其模块化程度降低。那时我还不如直接从内部事件循环中调用 Java 中的方法块。

4

1 回答 1

1

由于 JavaScript 是单线程的,因此选择介于

  1. 主线程中异步 运行一些代码,或者
  2. 在工作线程中运行相同的代码(即不是主线程的代码。

协同多任务

如果您想在主线程中运行繁重的代码而不会过度阻塞,则需要将其写入多任务协作。这需要长时间运行的同步任务定期将控制权交给任务管理器以允许其他任务运行。就 JavaScript 而言,您可以通过在异步函数中运行任务来实现此目的,该函数会定期等待短时间的系统计时器。这具有潜力,因为await在执行异步任务时保存了当前执行上下文并将控制权返回给任务管理器。计时器调用确保任务管理器在将控制权返回给启动计时器的异步任务之前实际上可以循环并执行其他操作。

等待已经履行的承诺只会在微任务队列中交错执行作业,而不会正确返回事件循环,并且不适合此目的。

调用代码模式:

   doWork()
   .then( data => console.log("work done"));

工作代码:

async function doWork() {

   for( i = 1; i < 10000; ++i) {
      // do stuff
      if( i%1000 == 0) {
         // let other things happen:
         await new Promise( resolve=>setTimeout(resolve, 4))
      }
   }
}

请注意,这借鉴了历史实践,可能适合让原型代码快速运行的目的。我不认为它特别适合商业生产环境。

工作线程

本地主机服务器可用于从 URL 提供工作代码,以便继续开发。一种常见的方法是使用节点/express 服务器在称为 localhost 的环回地址的端口上进行侦听。

您将需要安装node并使用 NPM(随 node 一起安装)安装express 。我无意进入 node/express 生态系统——网上有很多关于它的资料。

如果您仍在寻找一个极简的静态文件服务器来提供当前工作目录中的文件,这里是我之前写的一个。同样,网上也有许多类似的例子。

"use strict";
/* 
 * express-cwd.js
 * node/express server for files in current working directory
 * Terminal or shortcut/desktop launcher command:  node express-cwd
 */  

const express = require('express');
const path = require('path');
const process = require("process");

const app = express();
app.get( '/*', express.static( process.cwd())); // serve files from working directory

const ip='::1';  // local host
const port=8088; // port 8088

const server = app.listen(port, ip, function () {
  console.log( path.parse(module.filename).base + ' listening at http://localhost:%s', port);
})

承诺延迟

上面“工作代码”中显示的内联承诺延迟可以写成一个函数,而不是称为yield ,它是一个保留字。例如

const timeOut =  msec => new Promise( r=>setTimeout(r, msec));

分段执行阻塞代码的示例:

"use strict";

// update page every 500 msec

const counter = document.getElementById("count");
setInterval( ()=> counter.textContent = +counter.textContent + 1, 500);

function block_500ms() {
    let start = Date.now();
    let end = start + 500;
    for( ;Date.now() < end; );
}

// synchronously block for 4 seconds

document.getElementById("block")
.addEventListener("click", ()=> {
    for( var i = 8; i--; ) {
       block_500ms();
    }
    console.log( "block() done");
});

// block for 500 msec 8 times, with timeout every 500 ms

document.getElementById("block8")
.addEventListener("click", async ()=> {
    for( var i = 8; i--; ) {
       block_500ms();
       await new Promise( resolve=>setTimeout(resolve, 5)) 
    }
    console.log("block8() done");
});

const timeOut =  msec => new Promise( r=>setTimeout(r, msec));

document.getElementById("blockDelay")
.addEventListener("click", async ()=> {
    for( var i = 8; i--; ) {
       block_500ms();
       await timeOut(); 
    }
    console.log("blockDelay(1) done");
});
Up Counter: <span id="count">0</span>
<p>
<button id="block" type="button" >Block counter for 4 seconds</button> - <strong> with no breaks</strong>

<p>
<button id="block8" type="button" >Block for 4 seconds </button> - <strong> with short breaks every 500 ms (inline)</strong>
<p>
<button id="blockDelay" type="button" >Block for 4 seconds </button> - <strong> with short breaks every 500 ms (using promise function) </strong>

阻塞代码的交错部分可能会出现一些抖动,但可以避免完全冻结。超时值由实验确定 - 以可接受的方式工作的值越短越好。

警告

程序设计必须确保保存输入数据、中间结果和累积输出数据的变量不会被主线程代码破坏,主线程代码可能会或可能不会在繁重的代码执行过程中部分执行。

于 2022-02-19T12:21:55.040 回答