Home > OS >  Use Promise.all to expedite the excution of one asynced and one synced tasks - async await syntax
Use Promise.all to expedite the excution of one asynced and one synced tasks - async await syntax

Time:12-09

let's say I got three functions :

Asynchronous function - asyncF() - for example , it calls a REST API.

Synchronous function sync()

And executer function exec() which invokes them both as fast as possible , preferably in parallel .

I want exec() to be written using the "Async-await" syntax, without using the "Callbacks" or "Promises" syntax .

Does it make sense to write it as :

async exec(){
const [res1, res2]= await Promise.all([asyncF(), sync()])
...
}

Is there a better way?

CodePudding user response:

const p = asyncF();
const syncResult = sync();
const asyncResult = await p;

Start the async task, then run the sync function, then wait for the promise to resolve.

If the sync task takes longer, there would be no extra wait async one, as the promise will have resolved:

function sync() {
  const end = new Date();
  end.setSeconds(end.getSeconds()   3);
  //loop for 3 seconds
  for (let now = new Date(); now < end; now = new Date());
  
  return 2;
}

function asyncF() {
  //after 1 seconds return the value 40
  return new Promise(resolve => setTimeout(resolve, 1000, 40));
}

async function exec(){
  const p = asyncF();
  const syncResult = sync();
  const asyncResult = await p;
  
  return syncResult   asyncResult;
}

const start = new Date();
exec()
  .then(result => console.log(`result after ${new Date() - start}ms was: ${result}`));

If the async task takes longer, then there is an extra wait for the promise to resolve:

function sync() {
  //return immediately
  return 2;
}

function asyncF() {
  //after 3 seconds return the value 40
  return new Promise(resolve => setTimeout(resolve, 3000, 40));
}

async function exec(){
  const p = asyncF();
  const syncResult = sync();
  const asyncResult = await p;
  
  return syncResult   asyncResult;
}

const start = new Date();
exec()
  .then(result => console.log(`result after ${new Date() - start}ms was: ${result}`))

This assumes, of course, that the async task is properly asynchronous. An async function will still be executed synchronously until it reaches an await or ends.


On errors

Do note that async rejections should still be handled if that is a risk. The problem here is that if sync() fails, then the await p would never be reached, thus a code like

const p = asyncF();
try {
    const syncResult = sync();
    const asyncResult = await p;

    return syncResult   asyncResult;
} catch (error) { /* do something */}

will not properly handle the rejection coming from p.

For this we can take apage from Promise.allSettled() and safely wrap the promise results into an object with a status that tells whether it was fulfilled or rejected:

const p = asyncF()
    .then(value => ({ status: "fulfilled", value }))
    .catch(reason => ({ status: "rejected", reason });

This can further be encapsulated into small helpers:

const wrapPromise = p =>
    p.then(
        value => ({ status: "fulfilled", value }), 
        reason => ({ status: "rejected", reason })
    );

const unwrapPromise = p =>
  p.then((result) => {
    const {status, value, reason} = result;
    
    if (status === "fulfilled")
      return value;
    else if (status === "rejected")
      throw reason;
    
    throw new Error(`Unknown status ${status}, the value of the promise was ${result}`);
  });

Using const bar = wrapPromise(foo) would prevent unhandled promise rejections by handling them and await unwrapPromise(bar) will then trigger the failure to happen at that point when it can be handled:

const p = wrapPromise(asyncF());
try {
    const syncResult = sync();
    const asyncResult = await unwrapPromise(p);

    return syncResult   asyncResult;
} catch(error) { /* do something */ }

Thus if sync() fails first, there is no extra unhandled promise rejection going to happen later:

const wrapPromise = p =>
    p.then(
        value => ({ status: "fulfilled", value }), 
        reason => ({ status: "rejected", reason })
    );
const unwrapPromise = p =>
  p.then((result) => {
    const {status, value, reason} = result;
    
    if (status === "fulfilled")
      return value;
    else if (status === "rejected")
      throw reason;
    
    throw new Error(`Unknown status ${status}, the value of the promise was ${result}`);
  });

function sync() {
  const end = new Date();
  end.setSeconds(end.getSeconds()   1);
  //loop for 1 seconds
  for (let now = new Date(); now < end; now = new Date());
  
  throw new Error("sync() failed");
}

function asyncF() {
  //after 3 seconds reject with error the value 40
  return new Promise((_, reject) => setTimeout(reject, 3000, new Error("asyncF() failed")));
}

async function exec(){
  const p = wrapPromise(asyncF());
  try {
    const syncResult = sync();
    const asyncResult = await unwrapPromise(p);

    return syncResult   asyncResult;
  } catch(error) {
    throw new Error(`exec() failed because: ${error.message}`);
  }
}

const start = new Date();
exec()
  .then(result => console.log(`result after ${new Date() - start}ms was: ${result}`))
  .catch(error => console.error(`error after ${new Date() - start}ms was: ${error.message}`));

While if sync() succeeds after asyncF() has failed, that is still OK, since the await unwrapPromise(p) will be the point where an exception will be raised:

const wrapPromise = p =>
    p.then(
        value => ({ status: "fulfilled", value }), 
        reason => ({ status: "rejected", reason })
    );
const unwrapPromise = p =>
  p.then((result) => {
    const {status, value, reason} = result;
    
    if (status === "fulfilled")
      return value;
    else if (status === "rejected")
      throw reason;
    
    throw new Error(`Unknown status ${status}, the value of the promise was ${result}`);
  });

function sync() {
  const end = new Date();
  end.setSeconds(end.getSeconds()   3);
  //loop for 3 seconds
  for (let now = new Date(); now < end; now = new Date());
  
  return 2;
}

function asyncF() {
  //after 1 seconds reject with error the value 40
  return new Promise((_, reject) => setTimeout(reject, 1000, new Error("asyncF() failed")));
}

async function exec(){
  const p = wrapPromise(asyncF());
  try {
    const syncResult = sync();
    const asyncResult = await unwrapPromise(p);

    return syncResult   asyncResult;
  } catch(error) {
    throw new Error(`exec() failed because: ${error.message}`);
  }
}

const start = new Date();
exec()
  .then(result => console.log(`result after ${new Date() - start}ms was: ${result}`))
  .catch(error => console.error(`error after ${new Date() - start}ms was: ${error.message}`));


The async failure can be handled more simply if a fallback value is fine:

const p = asyncF().catch(error => {
    //optionally do something with error

    return -1; //fallback value
});

Then the await p will simply produce -1 on a failure.

CodePudding user response:

You can't really run two things in parallel

And it also depends on how your async function work, here async function will block an entire thread for almost a second:

exec()

async function exec() {
  const startTime = Date.now()
  const [res1, res2] = await Promise.all([asyncF(), sync()])
  console.log('finished', Date.now() - startTime, 'ms')
}

function asyncF() {
  console.log('start async')
  return Promise.resolve().then(() => {
    let i = 100000000;
    let x = 0
    while (i--) {
      x  = Math.random()
    }
    return 'async'
  })
}

function sync() {
  console.log('start sync')
  return 'sync'
}

About the question in the comment:

What Promise.all really do here is replace a map function

exec()

async function exec() {
  const values = await Promise.all([sync(), sync(), sync()])
  console.log(values)
  
  const values2 = [sync, sync, sync].map(fn => fn())
  console.log(values2)
}

function sync() {
  return Math.random()
}

Is it better? No

Is it faster? Also no

CodePudding user response:

The pattern is fine, basically the same as the pattern in this answer but with a bit of extra overhead for the array.

Consider this example:

const asyncF = () => {
  console.log("start async");
  return new Promise(resolve => setTimeout(() => {
    console.log("end async");
    resolve();
  }, 1000)); // nonblocking task for 1 second
};

const sync = () => {
  console.log("start sync");
  for (let i = 0; i < 100000; i  ); // blocking
  console.log("end sync");
};

const exec = () => Promise.all([asyncF(), sync()]);

(async () => {
  const startTime = Date.now();
  await exec();
  console.log("finished", Date.now() - startTime, "ms");
})()
  .catch(err => console.error(err));

You should see a result like:

start async
start sync
end sync
end async
finished 1012 ms

The parallelism occurs from the async task being a nonblocking I/O call like a fetch request (the whole point of async). If the async code was blocking, as shown in this answer, then there's no parallelism to be had since JS is single-threaded. But I assume your asyncFn is genuinely nonblocking.

  • Related