'How to run expensive code parallel in the same file

I'm trying to run a piece of JavaScript code asynchronously to the main thread. I don't necessarily need the code to actually run on a different thread (so performance does not need to be better that sequential execution), but I want the code to be executed in parallel to the main thread, meaning no freezing.

Additionally, all the code needed needs to be contained within a single function.

My example workload is as follows:

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

Additionally, I may have some work on the main thread (which is allowed to freeze the side, just for testing):

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

The expected output should be something like this:

Sequential
Async
Sequential
Sequential
Async
Sequential
Async
Async
...

You get the point.

Disclaimer: I am absolutely unexperienced in JavaScript and in working with async and await.

What I've tried

I did some research, and found these 3 options:

1. async/await

Seems like the obvious choice. So I tried this:

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

Output:

Async (100000)
Sequential (100000)

I also tried:

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

Output:

Async (100000)
Sequential (100000)

So both methods did not work. They also both freeze the browser during the async output, so it seems that that has absolutely no effect(?) I may be doing something very wrong here, but I don't know what.

2. Promise.all

This seems to be praised as the solution for any expensive task, but only seems like a reasonably choice if you have many blocking tasks and you want to "combine" them into just one blocking task that is faster than executing them sequentially. There are certainly use cases for this, but for my task it is useless, because I only have one task to execute asynchronously, and the main "thread" should keep running during that task.

3. Worker

This seemed to me like the most promising option, but I have not got it working yet. The main problem is that you seem to need a second script. I cannot do that, but even in local testing with a second file Firefox is blocking the loading of that script.


This is what I've tried, and I have not found any other options in my research. I'm starting to think that something like this is straight up not possible in JS, but it seems like a quite simple task. Again, I don't need this to be actually executed in parallel, it would be enough if the event loop would alternate between calling a statement from the main thread and the async "thread". Coming from Java, they are also able to simulate multi threading on a single hardware thread.


Edit: Context

I have some java code that gets converted to JavaScript (I have no control over the conversion) using TeaVM. Java natively supports multithreading, and a lot of my code relies on that being possible. Now since JavaScript apparently does not really support real multithreading, TeaVM converts Thread in the most simplistic way to JS: Calling Thread.start() directly calls Thread.run() which makes it completely unusable. I want to create a better multithreading emulation here which can - pretty much - execute the thread code basically without modification. Now it is not ideal but inserting "yielding" statements into the java code would be possible.

TeaVM has a handy feature which allows you to write native Java methods annotated with matching JS code that will be converted directly into that code. Problem is, you cannot set the method body so you can't make it an async method.

One thing I'm now trying to do is implement a JS native "yield" / "pause" (to not use the keyword in JS) function which I can call to allow for other code to run right from the java method. The method basically has to briefly block the execution of the calling code and instead invoke execution of other queued tasks. I'm not sure whether that is possible with the main code not being in an async function. (I cannot alter the generated JS code)

The only other way I can think of to work around this would be to let the JS method call all the blocking code, refering back to the Java code. The main problem though is, that this means splitting up the method body of the java method into many small chunks as Java does not support something like yield return from C#. This basically means a complete rework of every single parallel executed piece of code, which I would desperately try to avoid. Also, you could not "yield" from within a called method, making it way less modular. At that point I may as well just call the method chunks from within Java directly from an internal event loop.



Solution 1:[1]

Since JavaScript is single threaded the choice is between

  1. running some code asynchronously in the main thread, or
  2. running the same code in a worker thread (i.e. one that is not the main thread.

Coopererative Multitasking

If you want to run heavy code in the main thread without undue blocking it would need to be written to multitask cooperatively. This requires long running synchronous tasks to periodically yield control to the task manager to allow other tasks to run. In terms of JavaScript you could achieve this by running a task in an asynchronous function that periodically waits for a system timer of short duration. This has potential because await saves the current execution context and returns control to the task manager while an asynchronous task is performed. A timer call ensures that the task manager can actually loop and do other things before returning control to the asynchronous task that started the timer.

Awaiting a promise that is already fulfilled would only interleave execution of jobs in the microtask queue without returning to the event loop proper and is not a suitable for this purpose.

Calling code pattern:

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

Work code:

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))
      }
   }
}

Note this draws on historical practice and might suit the purpose of getting prototype code working quickly. I wouldn't think it particularly suitability for a commercial production environment.

Worker Threads

A localhost server can be used to serve worker code from a URL so development can proceed. A common method is to use a node/express server listening on a port of the loopback address known as localhost.

You will need to install node and install express using NPM (which is installed with node). It is not my intention to go into the node/express eco-system - there is abundant material about it on the web.

If you are still looking for a minimalist static file server to serve files from the current working directory, here's one I wrote earlier. Again there are any number of similar examples available on the net.

"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);
})

Promise Delays

The inlined promise delay shown in "work code" above can be written as a function, not called yield which is a reserved word. For example

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

An example of executing blocking code in sections:

"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>

Some jerkiness may be noticeable with interleaved sections of blocking code but the total freeze is avoided. Timeout values are determined by experiment - the shorter the value that works in an acceptable manner the better.

Caution

Program design must ensure that variables holding input data, intermediate results and accumulated output data are not corrupted by main thread code that may or may not be executed part way through the course of heavy code execution.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1