Live data from Hacker News

V8 adds support for top-level await

chromium.googlesource.com

161–170 of 310 posts

Re: V8 adds support for top-level await

#161

Earlier quoted context omitted.

You get that it's super common to await in the middle of something that could be considered a critical section, yeah? And that doing so will result in a different coroutine (for lack of a better term; that's what Promise-driven code effectively is) being executed until control returns to the awaited Promise within that critical section?

Yes but async/await is for concurrency, not parallelism. Only one or the other "coroutine" will run at any time so what is the lock protecting? There's no way for multiple branches of code to access the same variable at the same time in JS. What else would a reader/writer lock be used for?

Locks are for concurrency. Parallelism is orthogonal.

When you are forced to yield inside of your critical section (a database call, a file write, whatever), as is common in NodeJS, you must acquire a lock that another coroutine can't run through.

Re: V8 adds support for top-level await

#162
post #83

Earlier quoted context omitted.

Top level await does more than remove a main function. If you import modules that use top level await, they will be resolved before the imports finish. To me this is most important in node where it's not uncommon to do async operations during initialization. Currently you either have to export a promise or an async function.

Do we really want slow imports though? If you have a bunch of modules with async setup functions, would you not be able to Promise.all() them?

Sure, in many situations, but I guess if you need to setup things have have dependencies (init A then init B then init C) this will help.

Re: V8 adds support for top-level await

#163
post #78

Earlier quoted context omitted.

I think it's worth stopping to think in terms of "sequential lines of code". Even at CPU level, "sequential" instructions aren't, for a decade or so, to say nothing of xplicitly async code. One should think in terms of a dataflow graph, where data-independent nodes can run in any order, or in parallel. One should explicitly think about ordering of effects, and be explicit about effects in general. (Hence the rise of…

> Even at CPU level, "sequential" instructions aren't Within the context of the argument you are making, this is disingenuous. There's a big pile of transistors which determine whether it is safe to reorder those instructions.

Thar same pile of transistors, with a thick layer of software, determines whether it's safe to proceed a coroutine.

    const foo = async () {
      const ice = await freeze(water);
      const wCream = await whip(cream);
      const base = await pour(liquor, ice);
      const cocktail = await put(base, wCream);
      return cocktail.serve();
    }
In the above fragment, `freeze` and `whip` may run in any order or in parallel, you don't get to choose. Still `pour` never runs before `freeze` (though it can complete before `whip`), and `put` can only run last. This is because the above is syntactic sugar, and the dataflow graph gets encoded in the promises graph, with `.then` clauses giving an unequivocal dependency order where applicable.

Same in CPU: two loads can run in either order or in parallel, but an ADD that takes the result of both of them will only run when they both complete.

Re: V8 adds support for top-level await

#164

All this async code without decent locking primitives is leading to a rabbit hole of race conditions... It doesn't matter that it's all single threaded if all your function calls may or may not block and run a bunch of other code in the meantime, mutating all kinds of state. I feel like JavaScript developers of the 2020's are going to relearn the same things the C programmers of the 1990's learn, just a few levels of…

JavaScript uses non-preemptive multitasking. It's trivial to write locking primitives. Indeed there are already many: https://www.npmjs.com/search?q=mutex

You could even have a method decorator (https://github.com/tc39/proposal-decorators) that emulates Java's "synchronized" keyword.

Re: V8 adds support for top-level await

#165

Earlier quoted context omitted.

More subtly, here's an example of a hidden race condition in JS: async function totalSize(fol) { const files = await fol.getFiles(); let totalSize = 0; await Promise.all(files.map(async file => { totalSize += await file.getSize(); })); // totalSize is now way too small return totalSize; }

Because all the promises are waiting on `fileSize`, right? But do you mean that JS 1. will read `totalSize` then 2. do the asynchronous call, then 3. add and set? Seems like it's ambiguous and JS could just as easily read `totalSize` after the call, and all would be OK. Or is the ordering specified? Thanks for this clever example!

The ordering is specified left to right.

  totalSize += await getSize()
becomes

  totalSize = totalSize + await getSize()
So all the map callbacks run one by one, read totalSize as 0, and then suspend waiting for getSize(). Each one then resolves and assigns totalSize to be 0 + the size.

The race is what order the getSize() calls return in since only the last one will control the return value. Otherwise the issue isn't a race but just a logical ordering bug.

(This isn't super different than doing array[two()] = one() since two will actually run first, so ex. array[i += 1] = i will modify i before assigning the value.)

Correct would be to change the body of the map to:

  const fileSize = await file.getSize();
  totalSize += fileSize;
or the whole function to:

  async function totalSize(fol) {
    const files = await fol.getFiles();
    const sizes = await Promise.all(files.map(file => file.getSize()));
    return sizes.reduce((totalSize, size) => totalSize + size, 0);
  }

Re: V8 adds support for top-level await

#166

Earlier quoted context omitted.

Do we really want slow imports though? If you have a bunch of modules with async setup functions, would you not be able to Promise.all() them?

Sure, in many situations, but I guess if you need to setup things have have dependencies (init A then init B then init C) this will help.

How so? I'm not seeing the benefit over exporting A, B, and C as functions, and then putting them together in another spot (like a composite root for pure DI, or an IoC container, etc).

Is the argument for top-level await that you don't need the other spot? Because I feel like you still do - except now it's implicitly inside not only A, but likely B and C as well to some extent. And in a very inflexible way.

Re: V8 adds support for top-level await

#167

Earlier quoted context omitted.

More subtly, here's an example of a hidden race condition in JS: async function totalSize(fol) { const files = await fol.getFiles(); let totalSize = 0; await Promise.all(files.map(async file => { totalSize += await file.getSize(); })); // totalSize is now way too small return totalSize; }

Because all the promises are waiting on `fileSize`, right? But do you mean that JS 1. will read `totalSize` then 2. do the asynchronous call, then 3. add and set? Seems like it's ambiguous and JS could just as easily read `totalSize` after the call, and all would be OK. Or is the ordering specified? Thanks for this clever example!

[deleted]

Re: V8 adds support for top-level await

#168
post #83

Earlier quoted context omitted.

Top level await does more than remove a main function. If you import modules that use top level await, they will be resolved before the imports finish. To me this is most important in node where it's not uncommon to do async operations during initialization. Currently you either have to export a promise or an async function.

Do we really want slow imports though? If you have a bunch of modules with async setup functions, would you not be able to Promise.all() them?

That kinda stuff is typically an antipattern in C#; an async static "factory method" would be used(I use this pattern myself in Typescript). But I guess JavaScript has odd stuff like code chunking so the importau be pulling remote code and etc.

Re: V8 adds support for top-level await

#169

Earlier quoted context omitted.

How about an optimistic lock? async function deduct(amt) { var balance = await getBalance(); if (balance >= amt) await setBalance(balance - amt); var newbalance = await getBalance(); if (newbalance != (balance - amt)) { await setBalance(balance + amt); // tell the user the transaction failed... } }

This is getting off topic, but what would probably be best here is a test and set operation: setBalance(expectedBalance, newBalance)

How about an `updateBalance(updateFn)` that is protected by a mutex:

    async function deduct(amt) {
      await updateBalance(balance => {
        if (balance >= amt) {
          return balance - amt;
        } else {
          throw new Error("not enough balance");
      })
    }

Re: V8 adds support for top-level await

#170
post #96

Earlier quoted context omitted.

Since booleans can't be awaited until their state is negated/toggled, I'd say a lock that works like a lock is needed.

So use a Promise like a lock then. If it exists, wait on it. If not create one… Of course you don’t need to use the existence of a Promise as your Boolean in this case. You can simply use the Boolean state in addition to the Promise because your code is not going to yield while atomically setting one Boolean variable.

That would unblock all coroutines waiting for the promise, instead of just the first one. It's not that trivial, that's the point.
Post reply on HN