Async await: the worst thing to happen to programming?
andrewzuo.com
andrewzuo.com
But isn't the same true with go channels ? If you want to asynchronously interact with a channel (that is, without blocking the main thread), you have to do it in a go block and the caller has to do the same and so on ?
Promises behave similarly - must wrap your code in promises all the way.
These constructs are alternatives to the good old callbacks, which force you to write your code inside callbacks, thus 'infecting' everything and leading to callback hell.
This 'cascade infection' effect is due to the inherent nature of things happening asynchronously, which contradicts the synchronous program flow inside a thread, so when the async event terminates, the program has to jump to a handler in order to process the results.
In the end it's a matter of taste imo..
As some other comment said, it's like the Haskell IO monad and that's OK, because it lets you isolate and be aware of the implications of that code.
https://hackage.haskell.org/package/base-4.20.0.1/docs/Prelu...
Which ones? I think there's always some way to isolate, even if ugly.
Almost all of them? You need referential transparency (via laziness) too, otherwise your attempt at isolation will break at the first binding expression in a local scope for future processing elsewhere:
...
let arg = processData <$> ioAction
in ...
Do you want to wrap-and-call-later all of these cases into lambdas by hand? :)I'm not familiar with go, but I don't think so: stackful coroutines abstract better than the stackless kind.
In >95% of your code, it's fine to just write `foo_val <- foo_chan`, without spawning a goroutine. From a pragmatic standpoint, it's not really different from `foo_val = expensive_foo_calculation()`. This block of code is waiting for something else to finish, and the Go runtime is smart enough to decide whether or not this thread should be parked until that result is ready.
And, as a bonus, `foo_val = expensive_foo_calculation()` looks the same, even if the implementation launches 10 cpu-bound goroutines and reads from 30 files to do the work.
First of all, it gets the function color problem backwards. Async await forces 'coloring' of execution to be async. But the desired number of colors is one which is what you have without async/await.
The way Go solves this is by making all 'threads'/goroutines async context without saying so and there's no way to make them not that. Effectively they all started gray-purple or whatever color that was to begin with. It would be as if all the Rust developers went all in and said "Everyone let's only do async."
The problem isn't promises/futures etc, they work fine as can be seen in Java with their CompletionStage. You can even use Executors with thread pools without function-coloring. I never understood the need or desire for async/await keywords (and the corresponding 'rest-of-program' transformation that happens under the hood. That Rust adopted it is the main reason I won't consider entering the ecosystem unless it somehow gets sorted out e.g. with two library ecosystems, basically bifurcating the language.
I only played around with it a bit a long time ago, but I didn't find Go concurrency as simple and easy as it is often sold. It felt very low level, which is fine for the design goals of Go, but also meant that I was still left with doing some harder parts myself.
Concurrency in C# with async/await is pretty easy for the straightfoward cases that make up most of a typical application. You do have to keep to a few rules and it certainly has very dangerous footguns, but those are minimized if you use consistently use async methods instead of sync.
I think this is different for GUI apps, but I have no experience with that.
Ron Pressler always was an advocate for blocking code and even joined oracle to add virtual threads to the JVM, thus invalidating the performance argument of the async/await/non-blocking crowd.
I really wish young developers would be taught about the actor model and communicating sequential processes before falling for the false promises of async/await-land.
And I wish JavaScript runtimes had a way of expressing continuations /blocking threads on their eventloop.
Swift for example is transitioning from using a lot of callbacks and manual thread dispatch everywhere to using async/await and while the infection aspect is annoying from time to time, needing to deal with continuations/callbacks manually tends to be just as infectious, which was the old way. Even worse is manually wiring up message passing infrastructure inside the app. I wonder how Go UI libraries deal with this? I wrote some X11 apps in Go back in the day and had a bad time whenever I blocked the main thread waiting for a response from a background worker but maybe today there’s better abstractions in the native UI libraries.
The other big downside to threading is the mental overhead of needing to consider the memory model, worry about parallel memory access of objects causing problems, and needing to review code with a microscope in case someone is introducing memory access violations or the even worse deadlock/contention cases. Some programs really benefit from large shared data structures and those are fraught to share across threads and I think that’s where multi-threading gets its somewhat deserved reputation for being annoying.
Go is wonderful for the things it’s built in tools and semantics are well-suited to handle: request/response (where the UI lives in some other process that talks to Go; Go is always the “background” thread pool) or run-till-completed jobs that just print logs as their UI. It is kind of horrible for other stuff. I personally found the channel management and concurrency situation inside the Kubernetes source code really hard to follow since that’s a kind of program that’s all about long lived data shared structures & systems communicating with each other; it would probably be more understandable in Erlang or something.
Do all real work OOB and send messages of “state” into the rendering loop where all state can be reduced and then rendered into the interface.
And that’s a good thing.
In attempting to avoid the pollution u end up implementing the imperative shell/functional core pattern.
Most programmers don’t know that pattern. But for those in the know, the pollution is a good thing.
If you programmed in Haskell you know what’s up. The way you avoid the io monad from polluting everything is part of what makes the program so modular. Async does the same thing. Literally.
await b(await a())
The above is roughly equivalent to this in haskell: a >>= b
How do you avoid pollution? The answer to this question makes your program better.Also in Haskell you can only perform IO (aside from unsafe IO I guess) inside the IO monad, potentially making the abstraction worthwhile, this is not the case in many other languages.
An important difference between async/await and Haskell's `IO a` is that it's possible for asynchronous code to invoke sync code, and in some languages (such as Rust) vice-versa. So it acts more like a monad transformer, providing operations `IO a -> AsyncIO a` and `AsyncIO a -> IO a`.
The main challenge of async/await is that unskilled people who don't understand threads try to use async/await as a substitute, which leads to bizarre articles like "what color are your functions".
a >>= b
also known as monadic bind of a and b?It's not equivalent.
> How do you avoid pollution?
Haskell has <$> and the infrastructure of HKTs to stop this infectious propagation of IO, other languages do not, and their async/await colors do not isolate side-effectful actions from the rest pure parts of your codebase.
https://hackage.haskell.org/package/base-4.20.0.1/docs/Prelu...
The io monad does not isolate io from your pure code. It’s infectious just like an async function.
It’s the abstractions and ways to stop the infection that makes the code pure. You don’t even need hkts to do this. Most languages don’t have a type representing this infection. The infection propagates everywhere without anyone realizing it. The IO monad explicitly tells the developer that the infection is occurring.
I’m saying that async functions do the same thing as the io monad.
The <$> operator in Haskell is just sugar for patterns to stop the pollution from occurring. You can implement it in typescript too. It just won’t be as general as that operator is defined across functors. In typescript you would define a function across only promises.
> I’m saying that async functions do the same thing as the io monad.
> The <$> operator in Haskell is just sugar for patterns to stop the pollution from occurring.
No they don't. Async functions aren't IO actions in Haskell terms, and for the latter argument of <$>, you need referential transparency (via laziness) too, otherwise your attempt at "sugaring" your async functions will break at the first binding expression in a local scope for future processing elsewhere:
...
let arg = processData <$> ioAction
in ...
Do you want to wrap-and-call-later all of these cases into lambdas by hand? :) Show me an example of that being done in a type-safe way in typescript, and I'll point you at the layers that will break composition at the next binding.As for the rest of your argument, the point is to not use async functions locally in the context of pure logic. The pattern is imperative shell, functional core.
> the point is to not use async functions locally in the context of pure logic. The pattern is imperative shell, functional core.
The point is that IO actions aren't `async defs`, because async defs don't have two important properties to hold eqivalence to IO actions in Haskell. I'm not sure why you're trying to cherry pick arguments to see your argument fit into the slots that don't accept coloring keywords where they don't belong to: seamless composition.
IO actions aren’t equivalent to async defs. I never said that. I said roughly equivalent which means isomorphic.
I’m not sure why you’re trying to say I’m cherry picking my argument when I am the one dictating the point here. I made the first statement and you responded to it and you started out your previous response by trying to turn the conversation to your point.
Bro I made the point. I’m not changing the point. You need to not change the topic. In the very beginning I said functional core imperative shell. That’s the point.
I guess the io monad doesn’t prevent people from writing shit code in Haskell. You’re weaving in and out of io constantly with almost everything polluted with IO. Pure functions are scattered randomly in a patchwork of compositions without delineation between purity and IO. You don’t see that there needs to be a layer between the two.
I see you've been cultured by typescript and js.
> I said roughly equivalent which means isomorphic.
"roughly equivalent" isn't the definition of isomorphic, and I hinted which properties a type system and the runtime have to support for that isomorphism to be manifested in a language implementation, which isn't there for all of the mainstream languages, unless you're willing to provide that conversion by hand.
> when I am the one dictating the point here. I made the first statement and you responded to it and you started out your previous response by trying to turn the conversation to your point.
You're simply wrong, that happens.
> In the very beginning I said functional core imperative shell. That’s the point.
That terminology only exists as a coping mechanism for those on the mainstream languages. In Haskell everything is functional composition, and `IO a` is neither exempt from it, nor is made into a special case. When you realise this I'll congratulate you on becoming less ignorant.
Many people say the same when they see pro players in their game at the NFL's Super Bowl. Others get excited and pursuit the career.
1) async programming vs. threading
2) infectious async/await syntax
Async programming is great. Coroutines are a powerful tool, both for expressing your ideas more clearly and for improving performance in IO-heavy systems.
async/await syntax may not be the best design for async programming though. Consider example in Julia:
function foo(x)
@async print(x) # some IO function
end
function bar(x)
@sync foo(x)
end
`foo()` returns an asynchronous `Task`, `bar()` awaits this task, and you can invoke `bar()` from whatever context you want. Now look at the Python version with async/await keywords: async def foo(x):
print(x) # some IO function
def bar(x):
await foo(x)
# SyntaxError: 'await' outside async function
Oops, we can't make `bar()` synchronous, it MUST be `async` now, as well as all functions that invoke `bar()`. This is what is meant my "infectious" behavior.Maybe we can wrap it into `asyncio.run()` then and stop the async avalance?
def bar(x):
asyncio.run(foo(x))
bar(5)
Yes, it works in synchronous context. But path to asynchronous context is now closed for us: async def baz(x):
bar(x)
await baz(5)
# RuntimeError: asyncio.run() cannot be called from a running event loop
So in practice, whenever you change one of your functions to `async`, you have to change all its callers up the stack to also be `async`. And it hurts a lot.Can we have asynchronous programming in Python without async/await. Well, prior to Python 3.5 we used generators, so it looks like at least techically it's possible.
Gevent exists: https://sdiehl.github.io/gevent-tutorial/
It does not stem from async/await that Javascript doesn't have sleep()
function sleep(ms) {
return new Promise((resolve) =>
setTimeout(() => resolve(), ms)
);
}https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
Sleep() should halt, this returns immediately
If I want my code to actually halt, I can either make myself red and use await on your red function, or I resign myself to put everything after the sleep in a .then()
Atomics.wait(this.sleepBuffer, 0, 0, ms)>Thrown in one of the following cases:
>If the current thread cannot be blocked (for example, because it's the main thread).
I guess that's why it's not so widely used
Doing blocking IO together with UI code is pretty bad in general though. Disks are certainly not quick enough to have File.Delete(...) be blocking unless you know the disk isn't on a network server which is aboard a satellite leaving the solar system or whatever edge case you'll invariably run into.
Non-blocking IO without multithreading doesn't require async/await though, all operating systems have had non-blocking IO functions for decades, they just never made it into language stdlibs.
It works and is far more responsive than what we have today.
> Disks are certainly not quick enough to have File.Delete(...) be blocking
What if you invoke a delete and then it fails and you want the user to respond? What will the state of your UI be when that happens?
If you can't do anything else until you know whether it was a success or failure, then you ensure that. E.g. you disable every single button that allows the user to do something else before the previous operation completes. Basically the theory is usually that you can allow the UI to "read" the program state while a "write" operation is still in flight. Typically this results in the user being able to for example scroll a document so it re-renders correctly etc. After the in-flight operation succeeds/fails, you can show the user the message if required, then enable new operations to happen. But the UI never stopped pumping messages so it was always responsive at least.
Wow you mean the whole program becomes unresponsive? Crazy!
To address your main point, yes, scrolling, hover, etc can continue to work. But now you genuinely have two things your program is doing at once, and these must be coordinated.
A gui framework typically handles this, with a separate thread (or separate OS process). So your thread that responds to events can block while the render/refresh continues doing its thing.
With this design the problem goes away. Instead of writing code that disables the ui, issues a callback, waits to respond, you just literally write:
If (!file.delete()) { Showerror() }
This is the kind of code you can read, and put breakpoints on.
It is using all resources to do what it was told to.
Results depend on the magnitude of the task and the hardware available with a very large overlap where the difference doesn't matter at all.
If blowing up complexity everywhere to solve a problem you probably wont have is a good thing is left as an exercise for the reader.
I’m making fun of the notion that blocking = slow = unresponsive.
Yes a normal single threaded GUI normally becomes unresponsive if the user invokes blocking IO on the main (UI) thread. By "unresponsive" in this context I mean "does not process messages the message queue". That the user can't e.g. perform a certain operation is in this context not the same kind of "unresponsive". It responds (it could even tell him that he can't do it, or why he can't). It would be unresponsive if it gave the appearance that he could do something, but when he tries to, the UI doesn't respond and start the operation he requests.
> Instead of writing code that disables the ui, issues a callback, waits to respond, you just literally write: If (!file.delete()) { Showerror() }
That's typically how I'd write code regardless of whether it's explicitly async. "Disabling everything" usually isn't necessary, what you disable is of course the operations thare logically forbidden to perfom until the first operation completes. In a perfect world you don't have those. But often, you do.
I had lots of pending requests, the goal was to have as many as possible (since the whole job took about 30 min) without freezing the UI.
When the callback happens there is work to do. The pattern is to do this work immediately.
Then there were as many as [not] possible bits of work to do simultaneously. Since the amount of work per job is unpredictable deliberately making the amount of simultaneous jobs unpredictable is insanity.
Synchronously I can do [say] 50 requests per second, parse 55 and have a buffer.
The solution to the riddle is not to limit the number of requests by 90% and extend the task to take 5 hours while not using 90% of the resources. Then UI freezes only become less frequent, they don't go away.
Instead I store all data from all callbacks in an array along with a description and use a setInterval to parse a configurable number of responses per second while adjusting the new requests to the size of the backlog.
But then it isn't really async anymore.
People who use progressive languages[2] will be using effect systems in a year or two, and this problem will go away.
[1]: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
[2]: Unison, OCaml, Scala, and maybe more I don't know about.
True threaded programming is difficult. I find modern closure syntax, where closures can access parent contexts, to be the most effective way to write concurrent stuff.
In either case, you still need to worry about things like thread contention/locks and whatnot.
Those of us, of a certain age, can remember “refCon” (reference context) parameters. I haven’t had to use one of those, in ages.
It is true that an async/await task doesn't map 1:1 to an OS thread, but that's neither here nor there.
This is not correct.
Async is orthogonal to multithreading. The async runtime's threading model is an implementation detail. e.g. Node is single threaded. In Rust the Tokio async runtime has a configurable threading model.
The article is mostly focused on Dart and C# - maybe you're referring to one of those specific implementations
The fact that multiple lightweight threads map on an heavier weight OS thread like in Node or in Tokio or whatever is neither important nor novel. M:N threading has been a thing for a very long time.
A specialized async/await runtime is a bit different from the typical M:N runtime (which usually tries to transparently mimic the preemptive posix thread medal), but conceptually there isn't much difference.
you're using a definition of "thread" that is quite abstract and not at all what is generally meant by most people when discussing these things
> multiple lightweight threads map on an heavier weight thread like in Node
describing Node as implementing M:N threading, while correct in an abstract sense, is not really useful or again, how most people would describe it.
> A specialized async/await runtime is a bit different from the typical M:N runtime... but conceptually there isn't much difference.
sure, conceptually, but again, you're using definitions in a very idiosyncratic and abstract manner. Which is your right, but it's not very persuasive and it's out of touch with how most people talk about these things.
But maybe I'm missing something here. Do you know of an async runtime and a threaded runtime that do not have significant differences?
Worse than null-terminated strings in C? Worse than null being return on failure of dynamic memory allocation? Worse than nullability of columns in sql which Tony Hoare (the author) called "my Billion-dollar mistake"? Worse than the Knight Capital update bug that caused a $440million loss in 45minutes meaning Knight went out of business and was taken over? Worse than the innovative design of Therac-25 that caused deaths and serious injuries by giving patients 100x the intended doses of radiation? Worse than the Fujitsu Horizon system that lead to deaths due to stress and suicide, innocent people being put in jail etc...?
I could go on but you get my point.
What a staggeringly stupid headline in service of clickbait.
Horizon had really bad bugs, yes - but it wasn't the software that caused the cover-up or the miscarriages of justice: it was the management of (then-)privatized Post Office Ltd that decided they could not afford the risk of losing big government contracts if any word got-out that the system was making fundamental ledger errors. Most of everything else can be blamed on the sheer separation between the devs and the actual end-users: these problems could have been caught and fixed if the Horizon tech-support people weren't 100% subordinate to upper-management: if I learned that our support people were using phone-scripts that were as bad as the PO's I'd threaten to resign).
(though I'd argue the real fundamental problem here wasn't technical, nor managerial, but a simple consequence of the UK's entrenched class-system: subpostmasters generally don't read Classics at Oxbridge, which means in the eyes of the establishment that they're probably the ones at fault)
I'm not mad at the ICL/Fujitsu devs - they were severely understaffed (I gather it was literally just 4 people?), but I am disappointed (and in a state of disbelief) that they evidently didn't have any devs competent enough to know how to design a transactionally-safe retail POS system in the first place (and P.O.S. is the word...): hiring and retaining good people would have avoided this episode entirely (...though no-doubt something else would have led to a similar management scandal eventually - it's in the nature of almost all large UK businesses at this point.
Async/Await is a relatively recent development and the author is free to argue it's bad paradigm that makes simple things way harder than they need to. It wouldn't be the first time our industry jumped on a very silly bandwagon! Calling something "the worst" is fine. It's not a literal claim and it doesn't deserve your scorn.
CPS: https://en.wikipedia.org/wiki/Continuation-passing_style
Without any kind of async (this includes green threads) you run out of (OS) threads very fast.
This is not a black/white decision whether Async makes sense for every API all the time or never.
It's a solution to a specific problem that occurred (and still occurs) a lot.
The article was just about the syntax though, because they are still using asynchronous programming via coroutines (or goroutines ;-))
For all practical purposes goroutines behave as separate threads with blocking calls. The fact that they are multiplexed on a few system threads is an implementation detail.
Otherwise you could say that using system threads directly is also asynchrnous programming. After all, your thread gets suspended on system calls (including synchronization primitives) and is resumed upon their completion.
Semantically the biggest difference is that stackless coroutines typically require yield points to be marked syntactically in code.
What does run out mean?
Your program needs one million threads that sleep for 2 seconds, read some data and then finish. Guess what? Your execution is going to take hours, or get some kind of exception that you run out of threads because after the first ~6000 threads are taken, your OS can no longer give threads to anything else.
With Green threads, the threads are fake aka virtual and controlled by the language's runtime, be it JVM, CLR, Go's runtime etc. Runtime is usually smart enough to recognize sleeps and, while waiting for something, schedule another thread in its space.[1] So now all one million threads start near instantly and work almost all in parallel.
> Your program needs one million threads that sleep for 2 seconds, read some data and then finish.
I have yet to see this problem, but yeah I agree that millions is about when there will be problems.
So this would be possible:
async function f1() {
f2();
}
function f2() {
await fetch('something');
...
}
Instead of having to async each and every function and await each and every function call: async function f1() {
await f2();
}
async function f2() {
await fetch('something');
...
}
Which is what TFA complains about and what indeed is a pain in the ass.One could imagine a language where an await from sync code was syntactic sugar for such a function call, but generally the async/await syntax serves as a way to deliberately segregate your sync and async code. So that would defeat the purpose. At that point it might make more sense to design the language like Go and make everything async. (Preemptive runtimes like Java's virtual threads are another option.)
There was a good blog post I want to link here where the author argued that async/await is making explicit the fundamental property of some functions being expensive, as a counterpoint to TFA, but I haven't been able to find it again. But they made the case that expensive operations are fundamentally viral, and async/await was only making this explicit and wasn't unjustified overhead as some argue.
[1] https://docs.python.org/3/library/asyncio-task.html#asyncio....
function f1() {
x = await fetch('something');
...
}
Into this: function f1() {
fetch('something').then(x => {
...
});
}Also think how this should be translated: function f1() { x = await fetch('something'); return x + 1; }
Simple solution to concurrency.
I appreciate its hard to grasp at first but it’s literally second nature now I rarely need to even give it much thought.
I used to write a lot of asynchronous servers in C up until a couple of decades ago. I found it easy. Most people didn't. We have better ways of doing things today.
If it's the same CSP I'm thinking of, then yes, but it's only simpler because it relies on enough people on the team having a good grasp of these parts of CS. Based on my own experiences in uni I can tell that courses on formal-methods and the like are probably the least-popular: being taken by a tiny minority of students - it follows then that only a tiny minority of software-writing professionals will have the requisite level of understanding to apply these approaches to their day-job - and those that do are likely already employed within an organization that relies on these formal-methods (e.g. safety-critical avionics, Wall St. quants, etc) which in-turn will help attracts other highly-capable people.
...now contrast those imagined employers with the rest of the software industry: unsexy line-of-business application developers, SaaS dev contract shops, the places where people who couldn't get jobs at Google or OpenAI might end-up working; and also consider the larger-still community of non-professional software writers (people doing VBA in Excel to anyone who simply wants to learn how to make an interactive website for themselves).
CSP is not going to help this latter group. And, in fact, it's this very latter group which drives programming-language design because that market is 100x the size of the formal-methods-ivory-tower people (who are probably using gratis open-source tooling anyway).
Compared to CSP, async/Await is something that you can demonstrate to someone with almost zero experience writing software, who probably can't even afford the time to try to understand how it works, but the mechanics of putting the `await` keyword in the right place can be taught in a few hours and suits 95% of their needs.
-----
If languages like C# or JavaScript were designed only to suit people like you or me then the ecosystems around those languages wouldn't be anywhere near as big, nor those languages anywhere near as decently supported and maintained. If the "price" for that is putting-up with a handful of language-warts then I'm happy to make that trade. I've still got Z3 for everything else :)
This depends on what are your expectation. IO operations must suspend to wait for data read and writes, therefore it is not possible to avoid Async/Await. In other cases you might have multiple tasks depending on a specific IO operation, for example, one connection to a database that is used by multiple HTTP sessions, here it is also not possible to avoid Async/Await because those sessions are bound to an IO operation.
The real problem however comes when tasks that can complete synchrously are implemented with an asynchronous interface, for example
message = Await websocket.read(socket, buffer, ...)
This is a poor design because a socket read can pull multiple messages from the kernel buffers into user space and there should be a way to consumed them without Await. Many libraries however don't for watch this problem and that results in the everything is async madness.Well, for one example, it means the syntax for kicking off dozens of IO requests and collecting all the results is trivial.
Also I'm tired of people saying "async is infectious!" as if it is something clever.
Having concurrency be part of the type system is a good thing!
Isn't that covered in the paragraphs starting with?
And if so you could create some other way to run it asynchronously like Go’s go foo().Please show me this trivial code, assuming I want to process 12 requests in parallel at most (and always processing 12 at the same time until there's non left to process).
public async IAsyncEnumerable<HttpResponseMessage> SendTwelveRequestsAtATimeAsync(IAsyncEnumerable<HttpRequestMessage> requests)
{
HttpClient client = new();
List<HttpRequestMessage> requestsBatch = [];
await foreach (var req in requests)
{
if (requestsBatch.Count < 12)
{
requestsBatch.Add(req);
}
else
{
foreach (var res in await Task.WhenAll(requestsBatch.Select(client.SendAsync)))
{
yield return res;
}
requestsBatch = [];
}
}
}Promises are a primitive, and async and await keywords in JavaScript is just syntactic sugar around promises (it is sugar around similar constructs in other languages). A promise being just a long running task that will return a result eventually. Being able to grab a promise as an object and pass it around is super useful at times, and it is something I end up using a lot in my JS/TS code.
Because it is a language primitive that is also expressed in the type system, more complex systems can safely be built up around it, in the same way that it is easier to build safe(r) complex systems up around threads in languages that have threads as a primitive. (Rust being a great example here of bringing threads into the language, Java being another early example, though their early attempts were not perfect since we've learned a lot since 1995!)
Async/await and promises are a great example of a technology that makes doing easy stuff easy, and makes hard stuff possible.
tl;dr people need to stop complaining that other multitasking/threading paradigm looks different than their preferred one, each has plusses and minuses and one isn't "better" than others, they just serve different purposes.
let mut tasks = JoinSet::new();
let semaphore = Arc::new(Semaphore::new(12));
for i in 0..100 {
let semaphore = semaphore.clone();
tasks.spawn(async move {
let _permit = semaphore.acquire().await.unwrap();
sleep(Duration::from_secs(1)).await;
i * i // simulate some result
});
}
while let Some(result) = tasks.join_next_with_id().await {
let (id, result) = result?;
println!("Task {id} result: {result}");
}
That allows you to collect the results as they come in and align it to the various ids. If you only care about the final result and nothing why things are processing, let results = tasks.join_all();
The task is written inline there, but could as easily be an async function for i in 0..100 {
tasks.spawn(process_request(i, semaphore.clone()));
}https://www.npmjs.com/package/@node-rs/argon2
https://www.npmjs.com/package/bcrypt
Though the bcrypt package does provide an additional sync API. (You should be using argon2 though.)
The only real use case would be small scripts where you don't care about sync/async, but experience shows that devs will abuse the sync functions in scenarios where the async ones would be appropriate.
Unpopular opinion, it makes me want to have a setting where all function is async by default, and all function calls are await by default. With `nowait` and `sync` as the opposite
These days I think it's only one extra function call but still...
https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
import asyncio import something_else
...
asyncio.run(something_else.that_runs_asynchronously(x, z, z))
took care of this.