[HN Gopher] Asynchrony is not concurrency
___________________________________________________________________
Asynchrony is not concurrency
Author : kristoff_it
Score : 294 points
Date : 2025-07-18 19:21 UTC (1 days ago)
(HTM) web link (kristoff.it)
(TXT) w3m dump (kristoff.it)
| threatofrain wrote:
| IMO the author is mixed up on his definitions for concurrency.
|
| https://lamport.azurewebsites.net/pubs/time-clocks.pdf
| tines wrote:
| Can you explain more instead of linking a paper? I felt like
| the definitions were alright.
|
| > Asynchrony: the possibility for tasks to run out of order and
| still be correct.
|
| > Concurrency: the ability of a system to progress multiple
| tasks at a time, be it via parallelism or task switching.
|
| > Parallelism: the ability of a system to execute more than one
| task simultaneously at the physical level.
| threatofrain wrote:
| Concurrency is the property of a program to be divided into
| partially ordered or completely unordered units of execution.
| It does not describe how you actually end up executing the
| program in the end, such as if you wish to exploit these
| properties for parallel execution or task switching. Or maybe
| you're running on a single thread and not doing any task
| switching or parallelism.
|
| For more I'd look up Rob Pike's discussions for Go
| concurrency.
| tines wrote:
| The article understands this.
| threatofrain wrote:
| > Concurrency: the ability of a system to progress
| multiple tasks at a time, be it via parallelism or task
| switching.
|
| Okay, but don't go with this definition.
| michaelsbradley wrote:
| Is the hang up on _" at a time"_? What if that were
| changed to something like _" in a given amount of time"_?
|
| For single threaded programs, whether it is JS's event
| loop, or Racket's cooperative threads, or something
| similar, if Dt is small enough then only one task will be
| seen to progress.
| andsoitis wrote:
| > Asynchrony: the possibility for tasks to run out of order
| and still be correct.
|
| Asynchrony is when things don't happen at the same time or in
| the same phase, i.e. is the opposite of Synchronous. It can
| describe a lack of coordination or concurrence in time, often
| with one event or process occurring independently of another.
|
| The correctness statement is not helpful. When things happy
| asynchronously, you do not have guarantees about order, which
| may be relevant to "correctness of your program".
| w10-1 wrote:
| > The correctness statement is not helpful
|
| But... that's everything, and why it's included.
|
| Undefined behavior from asynchronous computing is not worth
| study or investment, except to avoid it.
|
| Virtually all of the effort for the last few decades (from
| super-scalar processors through map/reduce algorithms and
| Nvidia fabrics) involves enabling non-SSE operations that
| are correct.
|
| So yes, as an abstract term outside the context of
| computing today, asynchrony does not guarantee correctness
| - that's the difficulty. But the only asynchronous
| computing we care about offers correctness guarantees of
| some sort (often a new type, e.g., "eventually
| consistent").
| Lichtso wrote:
| Concurrency is parallelism and/or asynchrony, simply the
| superset of the other two.
|
| Asynchrony means things happen out of order, interleaved,
| interrupted, preempted, etc. but could still be just one
| thing at a time sequentially.
|
| Parallelism means the physical time spent is less that the
| sum of the total time spent because things happen
| simultaneously.
| jrvieira wrote:
| careful: in many programming contexts parallelism and
| concurrency are exclusive concepts, and sometimes under the
| umbrella of async, which is a term that applies to a
| different domain.
|
| in other contexts these words don't describe disjoint sets
| of things so it's important to clearly define your terms
| when talking about software.
| merb wrote:
| Yeah concurrency is not parallelism.
| https://go.dev/blog/waza-talk
| Lichtso wrote:
| That is compatible with my statement: Not all concurrency
| is parallelism, but all parallelism is concurrency.
| OkayPhysicist wrote:
| What people mean by "concurrency is not parallelism" is
| that they are different problems. The concurrency problem
| is defining an application such that it has parts that
| are not causally linked with some other parts of the
| program. The parallelism problem is the logistics of
| actually running multiple parts of your program at the
| same time. If I write a well-formed concurrent system, I
| shouldn't have to know or care if two specific parts of
| my system are actually being executed in parallel.
|
| In ecosystems with good distributed system stories, what
| this looks like in practice is that concurrency is your
| (the application developers') problem, and parallelism is
| the scheduler designer's problem.
| jlouis wrote:
| SIMD is parallel execution with no concurrency.
| Lichtso wrote:
| yes, some people swap the meaning of concurrency and
| asynchrony. But, almost all implementations of async use
| main event loops, global interpreter lock, co-routines
| etc. and thus at the end of the day only do one thing at
| a time.
|
| Therefore I think this definition makes the most sense in
| practical terms. Defining concurrency as the superset is
| a useful construct because you have to deal with the same
| issues in both cases. And differentiating asynchrony and
| parallelism makes sense because it changes the trade-off
| of latency and energy consumption (if the bandwidth is
| fixed).
| michaelsbradley wrote:
| Asynchrony also practically connotes nondeterminism, but a
| single-threaded concurrent program doesn't _have_ to
| exhibit nondeterministic behavior.
| jkcxn wrote:
| Not the OP, but in formal definitions like Communicating
| Sequential Processes, concurrency means the possibility for
| tasks to run out of order and still be correct, as long as
| other synchronisation events happen
| gowld wrote:
| Concurrency implies asynchrony (two systems potentially
| doing work at the same time withut waiting for each other),
| but the converse is not true.
|
| A single process can do work in an unordered (asynchronous)
| way.
| Zambyte wrote:
| Parallelism implies concurrency but not does not imply
| asynchrony.
| ryandv wrote:
| They're just different from what Lamport originally proposed.
| _Asynchrony_ as given is roughly equivalent to Lamport 's
| characterization of distributed systems as _partially
| ordered_ , where some pairs of events can't be said to have
| occurred before or after one another.
|
| One issue with the definition for concurrency given in the
| article would seem to be that no concurrent systems can
| deadlock, since as defined all concurrent systems can
| progress tasks. Lamport uses the word concurrency for
| something else: "Two events are concurrent if neither can
| causally affect the other."
|
| Probably the notion of (a)causality is what the author was
| alluding to in the "Two files" example: saving two files
| where order does not matter. If the code had instead been
| "save file A; read contents of file A;" then, similarly to
| the client connect/server accept example, the "save"
| statement and the "read" statement would not be concurrent
| under Lamport's terminology, as the "save" causally affects
| the "read."
|
| It's just that the causal relationship between two tasks is a
| different concept than how those tasks are composed together
| in a software model, which is a different concept from how
| those tasks are physically orchestrated on bare metal, and
| also different from the ordering of events..
| kazinator wrote:
| The definition of asynchrony is bad. It's possible for
| asynchronous requests to guarantee ordering, such that if a
| thread makes two requests A and B in that order,
| asynchronously, they will happen in that order.
|
| Asynchrony means that the requesting agent is not blocked
| while submitting a request in order to wait for the result
| of that request.
|
| Asynchronous abstractions may provide a synchronous way
| wait for the asynchronously submitted result.
| ryandv wrote:
| > The definition of asynchrony is bad. It's possible for
| asynchronous requests to guarantee ordering, such that if
| a thread makes two requests A and B in that order,
| asynchronously, they will happen in that order.
|
| It's true that it's possible - two async tasks can be
| bound together in sequence, just as with `Promise.then()`
| et al.
|
| ... but it's not _necessarily_ the case, hence the
| _partial_ order, and the " _possibility_ for tasks to
| run out of order ".
|
| For example - `a.then(b)` might bind tasks `a` and `b`
| together asynchronously, such that `a` takes place, and
| then `b` takes place - but after `a` has taken place, and
| before `b` has taken place, there may or may not be other
| asynchronous tasks interleaved between `a` and `b`.
|
| The ordering between `a`, `b`, and these interleaved
| events is not defined at all, and thus we have a partial
| order, in which we _can_ bind `a` and `b` together in
| sequence, but have no idea how these two events are
| ordered in relation to all the other asynchronous tasks
| being managed by the runtime.
| kazinator wrote:
| I mean that it's possible in the sense of being designed
| in as a guarantee; that the async operations issued
| against some API object will be performed in the order in
| which they are submitted, like a FIFO queue.
|
| I don't mean "promise.then", whereby the issuance of the
| next request is gated on the completion of the first.
|
| An example might be async writes to a file. If we write
| "abc" at the start of the file in one request and "123"
| starting at the second byte in the second requests, there
| can be a guarantee that the result will be "a123", and
| not "abc2", without gating on the first request
| completing before starting the other.
|
| async doesn't mean out of order; it means the request
| initiator doesn't synchronize on the completion as a
| single operation.
| amelius wrote:
| > Asynchrony: the possibility for tasks to run out of order
| and still be correct.
|
| Can't we just call that "independent"?
| skydhash wrote:
| Not really. There may be some causal relations.
| amelius wrote:
| Can you give an example?
| skydhash wrote:
| Locks, scheduling,... That introduce some synchronicity
| and so some kind of order. But it's enforced on the
| system and not a required mechanism.
| amelius wrote:
| But if I run "ls" on a machine, and another user runs
| "ls" on the same machine, wouldn't you consider them
| independent, even though the OS uses all kinds of locks
| and what not under the hood?
| ninetyninenine wrote:
| Doesn't multiple tasks at the same time make it simultaneous?
|
| I think there needs to be a stricter definition here.
|
| Concurrency is the ability of a system to chop a task into
| many tiny tasks. A side effect of this is that if the system
| chops all tasks into tiny tasks and runs them all in a sort
| of shuffled way it looks like parallelism.
| sriram_malhar wrote:
| The phrase "multiple tasks at a time" is ill-defined,
| according to Lamport, because whose clock are you trusting.
|
| For lamport concurrent does not mean what it means to us
| colloquially or informally (like, "meanwhile"). Concurrency
| in Lamport's formal definition is only about order. If one
| task is dependent or is affected by another, then the first
| is ordered after the second one. Otherwise, they are deemed
| to be "concurrent", even if one happens years later or
| before.
| sapiogram wrote:
| This is why I've completely stopped using the term, literally
| everyone I talk to seems to have a different understanding. It
| no longer serves any purpose for communication.
| carodgers wrote:
| The author is aware that definitions exist for the terms he
| uses in his blog post. He is proposing revised definitions. As
| long as he is precise with his new definitions, this is fine.
| It is left to the reader to decide whether to adopt them.
| WhitneyLand wrote:
| He's repurposing asynchrony that's different from the way
| most literature and many developers use it, and that shift is
| doing rhetorical work to justify a particular Zig API split.
|
| No thanks.
| laserbeam wrote:
| Keep in mind that you can't really express half the concepts in
| lamport's papers in most languages. You don't really talk about
| total and partial clock ordering when starting a thread. You
| only really do it in TLA+ when designing a protocol.
|
| That being said, I agree we don't need a new term to express
| "Zig has a function in the async API that throws a compilation
| error when you run in a non-concurrent execution. Zig let's you
| say that." It's fine to so that without proposing new theory.
| ioasuncvinvaer wrote:
| Is there anything new in this article?
| butterisgood wrote:
| Perhaps not, but sometimes the description from a different
| angle helps somebody understand the concepts better.
|
| I don't know how many "monad tutorials" I had to read before it
| all clicked, and whether it ever fully clicked!
| gowld wrote:
| Most gen ed articles on HN are not new ideas, just articles
| that could be pages from a textbook.
| ltbarcly3 wrote:
| The argument about concurrency != parallelism mentioned in this
| article as being "not useful" is often quoted and rarely a useful
| or informative, and it also fails to model actual systems with
| enough fidelity to even be true in practice.
|
| Example: python allows concurrency but not parallelism. Well not
| really though, because there are lots of examples of parallelism
| in python. Numpy both releases the GIL and internally uses open-
| mp and other strategies to parallelize work. There are a thousand
| other examples, far too many nuances and examples to cover here,
| which is my point.
|
| Example: gambit/mit-scheme allows parallelism via parallel
| execution. Well, kindof, but really it's more like python's
| multiprocess library pooling where it forks and then marshals the
| results back.
|
| Besides this, often parallel execution is just a way to manage
| concurrent calls. Using threads to do http requests is a simple
| example, while the threads are able to execute in parallel
| (depending on a lot of details) they don't, they spend almost
| 100% of their time blocking on some socket.read() call. So is
| this parallelism or concurrency? It's what it is, it's threads
| mostly blocking on system calls, parallelism vs concurrency gives
| literally no insights or information here because it's a
| pointless distinction in practice.
|
| What about using async calls to execute processes? Is that
| concurrency or parallelism? It's using concurrency to allow
| parallel work to be done. Again, it's both but not really and you
| just need to talk about it directly and not try to simplify it
| via some broken dichotomy that isn't even a dichotomy.
|
| You really have to get into more details here, concurrency vs
| parallelism is the wrong way to think about it, doesn't cover the
| things that are actually important in an implementation, and is
| generally quoted by people who are trying to avoid details or
| seem smart in some online debate rather than genuinely problem
| solving.
| throwawaymaths wrote:
| edit: fixed by the author.
| ltbarcly3 wrote:
| yes I'm agreeing with the article completely
| throwawaymaths wrote:
| maybe change 'this' to a non-pronoun? like "rob pikes
| argument"
| ltbarcly3 wrote:
| updated, thanks for the feedback
| throwawaymaths wrote:
| thanks for clarifying!
| chowells wrote:
| The difference is quite useful and informative. In fact, most
| places don't seem to state it strongly enough: Concurrency is a
| programming model. Parallelism is an execution model.
|
| Concurrency is writing code with the appearance of multiple
| linear threads that can be interleaved. Notably, it's about
| _writing code_. Any concurrent system could be written as a
| state machine tracking everything at once. But that 's really
| hard, so we define models that allow single-purpose chunks of
| linear code to interleave and then allow the language,
| libraries, and operating system to handle the details. Yes,
| even the operating system. How do you think multitasking worked
| before multi-core CPUs? The kernel had a fancy state machine
| tracking execution of multiple threads that were allowed to
| interleave. (It still does, really. Adding multiple cores only
| made it more complicated.)
|
| Parallelism is running code on multiple execution units. That
| is _execution_. It doesn 't matter how it was written; it
| matters how it executes. If what you're doing can make use of
| multiple execution units, it can be parallel.
|
| Code can be concurrent without being parallel (see async/await
| in javascript). Code can be parallel without being concurrent
| (see data-parallel array programming). Code can be both, and
| often is intended to be. That's because they're describing
| entirely different things. There's no rule stating code must be
| one or the other.
| ltbarcly3 wrote:
| When you define some concepts, those definitions and concepts
| should help you better understand and simplify the
| descriptions of things. That's the point of definitions and
| terminology. You are not achieving this goal, quite the
| opposite in fact, your description is confusing and would
| never actually be useful in understanding, debugging, or
| writing software.
|
| Stated another way: if we just didn't talk about concurrent
| vs parallel we would have exactly the same level of
| understanding of the actual details of what code is doing,
| and we would have exactly the same level of understanding
| about the theory of what is going on. It's trying to impose
| two categories that just don't cleanly line up with any real
| system, and it's trying to create definitions that just
| aren't natural in any real system.
|
| Parallel vs concurrent is a bad and useless thing to talk
| about. It's a waste of time. It's much more useful to talk
| about what operations in a system can overlap each other in
| time and which operations cannot overlap each other in time.
| The ability to overlap in time might be due to technical
| limitations (python GIL), system limitations (single core
| processor) or it might be intentional (explicit locking), but
| that is the actual thing you need to understand, and parallel
| vs concurrent just gives absolutely no information or
| insights whatsoever.
|
| Here's how I know I'm right about this: Take any actual
| existing software or programming language or library or
| whatever, and describe it as parallel or concurrent, and then
| give the extra details about it that isn't captured in
| "parallel" and "concurrent". Then go back and remove any
| mention of "parallel" and "concurrent" and you will see that
| everything you need to know is still there, removing those
| terms didn't actually remove any information content.
| chowells wrote:
| Addition vs multiplication is a bad and useless thing to
| talk about. It's a waste of time. It's much more useful to
| talk about what number you get at the end. You might get
| that number from adding once, or twice, or even more times,
| but that final number is the actual thing you need to
| understand and "addition vs multiplication" just gives
| absolutely no information or insights whatsoever.
|
| They're just different names for different things. Not
| caring that they're different things makes communication
| difficult. Why do that to people you intend to communicate
| with?
| gf000 wrote:
| That leaves out the (partial) ordering of "operations".
| Happens before/after/at the same time is just one important
| primitive.
| frollogaston wrote:
| Async JS code is parallel too. For example, await
| Promise.all(...) will wait on multiple functions at once. The
| JS event loop is only going to interpret one statement at a
| time, but in the meantime, other parts of the computer (file
| handles, TCP/IP stack, maybe even GPU/CPU depending on the JS
| lib) are actually doing things fully in parallel. A more
| useful distinction would be, the JS interpreter is single-
| threaded while C code can be multithreaded.
|
| I can't think of anything in practice that's concurrent but
| not parallel. Not even single-core CPU running 2 threads,
| since again they can be using other resources like disk in
| parallel, or even separate parts of the CPU itself via
| pipelining.
| filleduchaos wrote:
| > A more useful distinction would be, the JS interpreter is
| single-threaded while C code can be multithreaded.
|
| ...this seems like a long way round to say "JS code is not
| parallel while C code can be parallel".
|
| Or to put it another way, it seems fairly obvious to me
| that parallelism is a concept applied to _one 's own code_,
| not all the code in the computer's universe. Other parts of
| the computer doing other things has nothing to do with the
| point, or "parallelism" would be a completely redundant
| concept in this age where nearly every CPU has multiple
| cores.
| Retr0id wrote:
| I don't get it - the "problem" with the client/server example in
| particular (which seems pivotal in the explanation). But I am
| also unfamiliar with zig, maybe that's a prerequisite. (I am
| however familiar with async, concurrency, and parallelism)
| koakuma-chan wrote:
| Example 1
|
| You can write to one file, wait, and then write to the second
| file.
|
| Concurrency not required.
|
| Example 2
|
| You can NOT do Server.accept, wait, and then do Client.connect,
| because Server.accept would block forever.
|
| Concurrency required.
| tines wrote:
| Oh, I see. The article is saying that async is required. I
| thought it was saying that parallelism is required. The way
| it's written makes it seem like there's a problem with the
| code sample, not that the code sample is correct.
| Retr0id wrote:
| The article later says (about the server/client example)
|
| > Unfortunately this code doesn't express this requirement
| [of concurrency], which is why I called it a programming
| error
|
| I gather that this is a quirk of the way async works in
| zig, because it would be correct in all the async runtimes
| I'm familiar with (e.g. python, js, golang).
|
| My existing mental model is that "async" is just a
| syntactic tool to express concurrent programs. I think I'll
| have to learn more about how async works in zig.
| sunshowers wrote:
| I think a key distinction is that in many application-
| level languages, each thing you await exists autonomously
| and keeps doing things in the background whether you
| await it or not. In system-level languages like Rust (and
| presumably Zig) the things you await are generally
| passive, and only make forward progress if the caller
| awaits them.
|
| This is an artifact of wanting to write async code in
| environments where "threads" and "malloc" aren't
| meaningful concepts.
|
| Rust does have a notion of autonomous existence: tasks.
| Retr0id wrote:
| Thanks, this clears things up for me.
|
| I suppose I conflated "asynchrony" (as defined in the
| article) and "async" as a syntax feature in languages I'm
| familiar with.
| m11a wrote:
| I think that notion is very specific to Rust's design.
|
| Golang for example doesn't have that trait, where the
| user (or their runtime) must drive a future towards
| completion by polling.
| sunshowers wrote:
| Right, but Go is an application-level language and
| doesn't target environments where threads aren't a
| meaningful concept. It's more an artifact of wanting to
| target embedded environments than something specific to
| Rust.
| jayd16 wrote:
| Thank you so much for the added context. This explains
| the article very well.
| jayd16 wrote:
| But why is this a novel concept? The idea of starvation is
| well known and you don't need parallelism for it to effect
| you already. What does zig actually do to solve this?
|
| Many other languages could already use async/await in a
| single threaded context with an extremely dumb scheduler that
| never switches but no one wants that.
|
| I'm trying to understand but I need it spelled out why this
| is interesting.
| shikon7 wrote:
| The novel concept is to make it explicit where a non-
| concurrent scheduler is enough (async), and where it is not
| (async-concurrent). As a benefit, you can call async
| functions directly from a synchronous context, which is not
| possible for the usual async/await, therefore avoiding the
| need to have both sync and async versions of every
| function.
|
| And with green threads, you can have a call chain from
| async to sync to async, and still allow the inner async
| function to yield through to the outer async function. This
| keeps the benefit of async system calls, even if the
| wrapping library only uses synchronous functions.
| danaugrs wrote:
| Excellent article. I'm looking forward to Zig's upcoming async
| I/O.
| butterisgood wrote:
| It's kind of true...
|
| I can do a lot of things asynchronously. Like, I'm running the
| dishwasher AND the washing machine for laundry at the same time.
| I consider those things not occurring at "the same time" as
| they're independent of one another. If I stood and watched one
| finish before starting the other, they'd be a kind of synchronous
| situation.
|
| But, I also "don't care". I think of things being organized
| concurrently by the fact that I've got an outermost orchestration
| of asynchronous tasks. There's a kind of governance of
| independent processes, and my outermost thread is what turns the
| asynchronous into the concurrent.
|
| Put another way. I don't give a hoot what's going on with your
| appliances in your house. In a sense they're not synchronized
| with my schedule, so they're asynchronous, but not so much
| "concurrent".
|
| So I think of "concurrency" as "organized asynchronous
| processes".
|
| Does that make sense?
|
| Ah, also neither asynchronous nor concurrent mean they're
| happening at the same time... That's parallelism, and not the
| same thing as either one.
|
| Ok, now I'll read the article lol
| didibus wrote:
| I think asynchronous as meaning out-of-sync, implies that there
| needs to be synchronicity between the two tasks.
|
| In that case, asynchronous just means the state that two or
| more tasks that should be synchronized in some capacity for the
| whole behavior to be as desired, is not properly in-sync, it's
| out-of-sync.
|
| Then I feel there can be many cause of asynchronous behavior,
| you can be out-of-sync due to concurent execution or due to
| parallel execution, or due to buggy synchronization, etc.
|
| And because of that, I consider asynchronous programming as the
| mechanisms that one can leverage to synchronize asynchronous
| behavior.
|
| But I guess you could also think of asynchronous as doesn't
| need to be synchronized.
|
| Also haven't read the article yet lol
| criddell wrote:
| > I consider those things not occurring at "the same time" as
| they're independent of one another.
|
| What would it take for you to consider them as running at the
| same time then?
| kristoff_it wrote:
| That eventually the server and the client are able to
| connect.
| criddell wrote:
| The dishwasher and washing machine?
| throwawaymaths wrote:
| a core problem is that the term async itself is all sorts of
| terrible, synchronous usually means "happening at the same time",
| is _not_ what is happening when you don 't use `async`
|
| its like the whole flammable/inflammable thing
| butterisgood wrote:
| > Concurrency refers to the ability of a system to execute
| multiple tasks through simultaneous execution or time-sharing
| (context switching)
|
| Wikipedia had the wrong idea about microkernels for about a
| decade too, so ... here we are I guess.
|
| It's not a _wrong_ description but it's incomplete...
|
| Consider something like non-strict evaluation, in a language like
| Haskell. One can be evaluating thunks from an _infinite_
| computation, terminate early, and resume something else just due
| to the evaluation patterns.
|
| That is something that could be simulated via generators with
| "yield" in other languages, and semantically would be pretty
| similar.
|
| Also consider continuations in lisp-family languages... or
| exceptions for error handling.
|
| You have to assume all things could occur simultaneously relative
| to each other in what "feels like" interrupted control flow to
| wrangle with it. Concurrency is no different from the outside
| looking in, and sequencing things.
|
| Is it evaluated in parallel? Who knows... that's a strategy that
| can be applied to concurrent computation, but it's not required.
| Nor is "context switching" unless you mean switched control flow.
|
| The article is very good, but if we're going by the "dictionary
| definition" (something programming environments tend to get only
| "partially correct" anyway), then I think we're kind of missing
| the point.
|
| The stuff we call "asynchronous" is usually a subset of
| asynchronous things in the real world. The stuff we treat as task
| switching is a single form of concurrency. But we seem to all
| agree on parallelism!
| raluk wrote:
| One thing that most languages are lacking is expressing lazy
| return values. -> await f1() + await f2() and to express this
| concurently requres manually handing of futures.
| sedatk wrote:
| Which languages do have such a thing?
| Twey wrote:
| I suppose Haskell does, as `(+) <$> f1 <*> f2`.
| raluk wrote:
| In there is also ApplicativeDo that works nicely with this.
| do x <- f1 y <- f2 return $
| x + y
|
| this is evaluated as applicative in same way.
| steveklabnik wrote:
| Rust does this, if you don't call await on them. You can then
| await on the join of both.
| sedatk wrote:
| Is the "join" syntax part of the language?
| tcfhgj wrote:
| no
|
| https://doc.rust-lang.org/std/future/macro.join.html
| sedatk wrote:
| Then it doesn't apply in this case.
| tcfhgj wrote:
| why?
| deathanatos wrote:
| Why is having it be syntax necessary or beneficial?
|
| One might say "Rust's existing feature set makes this
| possible already, why dedicate syntax where none is
| needed?"
|
| (...and I think that's a reasonably pragmatic stance,
| too. Joins/selects are somewhat infrequent, the
| impediments that writing out a join puts on the program
| relatively light... what problem would be solved?
|
| vs. `?`, which sugars a _common_ thing that non-dedicated
| syntax can represent (a try! macro is sufficient to
| replace ?) but for which the burden on the coder is much
| higher, in terms of code readability & writability.)
| zmj wrote:
| That's because f2's result could depend on whether f1 has
| executed.
| jayd16 wrote:
| you mean like? await Join(f1(), f2())
|
| Although more realistically Promise1 = f1();
| Promise2 = f2(); await Join(Promise1, Promise2);
|
| But also, futures are the expression of lazy values so I'm not
| sure what else you'd be asking for.
| raluk wrote:
| This is what i hand in mind whit "manually handing of
| futures". In this case you have to write
| Promise1 = f1(); Promise2 = f2(); v1,v2 = await
| Join(Promise1, Promise2); return v1 + v2
|
| I think this is just too much of synthactic noise.
|
| On the other hand, it is necessary becase some of underlying
| async calls can be order dependend.
|
| for example await sock.rec(1) == 'A' &&
| await sock.rec(1) == 'B'
|
| checks that first received socket byte is A and second is B.
| This is clearly order dependant that can't be executed
| concurrently out of order.
| jayd16 wrote:
| I suppose you'd have to make
| SumAsync(F1(),f2());
|
| Buy it's kind of intractable, isn't it? Your language has
| to assume order dependency or independency and specify the
| other. Most seem to stick with lexical ordering implies
| execution order.
|
| I think some use curly brace scoping to break up
| dependency. I want to say kotlin does something like this.
|
| This is why they say async is a viral pattern but IMO
| that's because you're adding specificity and function
| coloring is necessary and good.
| kobzol wrote:
| The new Zig I/O idea seems like a pretty ingenious idea, if you
| write mostly applications and don't need stackless coroutines. I
| suspect that writing libraries using this style will be quite
| error-prone, because library authors will not know whether the
| provided I/O is single or multi-threaded, whether it uses evented
| I/O or not... Writing concurrent/async/parallel/whatever code is
| difficult enough on its own even if you have perfect knowledge of
| the I/O stack that you're using. Here the library author will be
| at the mercy of the IO implementation provided from the outside.
| And since it looks like the IO interface will be a proper kitchen
| sink, essentially an implementation of a "small OS", it might be
| very hard to test all the potential interactions and combinations
| of behavior. I'm not sure if a few async primitives offered by
| the interface will be enough in practice to deal with all the
| funny edge cases that you can encounter in practice. To support a
| wide range of IO implementations, I think that the code would
| have to be quite defensive and essentially assume the most
| parallel/concurrent version of IO to be used.
|
| It will IMO also be quite difficult to combine stackless
| coroutines with this approach, especially if you'd want to avoid
| needless spawning of the coroutines, because the offered
| primitives don't seem to allow expressing explicit polling of the
| coroutines (and even if they did, most people probably wouldn't
| bother to write code like that, as it would essentially boil down
| to the code looking like "normal" async/await code, not like Go
| with implicit yield points). Combined with the dynamic dispatch,
| it seems like Zig is going a bit higher-level with its language
| design. Might be a good fit in the end.
|
| It's quite courageous calling this approach "without any
| compromise" when it has not been tried in the wild yet - you can
| claim this maybe after 1-2 years of usage in a wider ecosystem.
| Time will tell :)
| ta8645 wrote:
| > It will IMO also be quite difficult to combine stackless
| coroutines with this approach
|
| Maybe there will be unforeseen problems, but they have promised
| to provide stackless coroutines; since it's needed for the WASM
| target, which they're committed to supporting.
|
| > Combined with the dynamic dispatch
|
| Dynamic dispatch will only be used if your program employs more
| than one IO implementation. For the common case where you're
| only using a single implementation for your IO, dynamic
| dispatch will be replaced with direct calls.
|
| > It's quite courageous calling this approach "without any
| compromise" when it has not been tried in the wild yet.
|
| You're right. Although it seems quite close to what "Jai" is
| purportedly having success with (granted with an implicit IO
| context, rather than an explicitly passed one). But it's
| arguable if you can count that as being in the wild either...
| necovek wrote:
| > I think that the code would have to be quite defensive and
| essentially assume the most parallel/concurrent version of IO
| to be used.
|
| Exactly, but why would anyone think differently when the goal
| is to support both synchronous and async execution?
|
| However, if asynchrony is done well at the lower levels of IO
| event handler, it should be simple to implemcent by following
| these principles everywhere -- the "worst" that could happen is
| that your code runs sequentially (thus slower), but not run
| into races or deadlocks.
| sedatk wrote:
| Blocking async code is not async. In order for something to
| execute "out of order", you must have an escape mechanism from
| that task, and that mechanism essentially dictates a form of
| concurrency. Async must be concurrent, otherwise it stops being
| async. It becomes synchronous.
| vitaminCPP wrote:
| This is exactly what the article is trying to debunk.
| didibus wrote:
| If you need to do A and then B in that order, but you're doing
| B and then A. It doesn't matter if you're doing B and then A in
| a single thread, the operations are out of sync.
|
| So I guess you could define this scenario as asynchronous.
| jayd16 wrote:
| So wait, is the word they mean by asynchrony actually the
| word "dependency"?
| Jtsummers wrote:
| > So wait, is the word they mean by asynchrony actually the
| word "dependency"?
|
| No, the definition provided for asynchrony is:
|
| >> Asynchrony: the possibility for tasks to run out of
| order and still be correct.
|
| Which is not dependence, but rather _in_ dependence.
| Asynchronous, in their definition, is concurrent with no
| need for synchronization or coordination between the tasks.
| The contrasted example which is still concurrent but not
| asynchronous is the client and server one, where the order
| matters (start the server after the client, or terminate
| the server before the client starts, and it won't work
| correctly).
| kristoff_it wrote:
| > The contrasted example which is still concurrent but
| not asynchronous is the client and server one
|
| Quote from the post where the opposite is stated:
|
| > With these definitions in hand, here's a better
| description of the two code snippets from before: both
| scripts express asynchrony, but the second one requires
| concurrency.
|
| You can start executing Server.accept and Client.connect
| in whichever order, but both must be running "at the same
| time" (concurrently, to be precise) after that.
| Jtsummers wrote:
| Your examples and definitions don't match then.
|
| If asynchrony, as I quoted direct from your article,
| insists that order doesn't matter then the client and
| server are not asynchronous. If the client were to
| execute before the server and fail to connect (the server
| is not running to accept the connection) then your system
| has failed, the server will run later and be waiting
| forever on a client who's already died.
|
| The client/server example is _not_ asynchronous by your
| own definition, though it is concurrent.
|
| What's needed is a fourth term, synchrony. Tasks which
| are concurrent (can run in an interleaved fashion) but
| where order between the tasks matters.
| kristoff_it wrote:
| > If the client were to execute before the server and
| fail to connect (the server is not running to accept the
| connection) then your system has failed, the server will
| run later and be waiting forever on a client who's
| already died.
|
| From the article:
|
| > Like before, the order doesn't matter: the client could
| begin a connection before the server starts accepting
| (the OS will buffer the client request in the meantime),
| or the server could start accepting first and wait for a
| bit before seeing an incoming connection.
|
| When you create a server socket, you need to call
| `listen` and after that clients can begin connecting. You
| don't need to have already called `accept`, as explained
| in the article.
| jayd16 wrote:
| > Which is not dependence, but rather independence
|
| Alright, well, good enough for me. Dependency tracking
| implies independency tracking. If that's what this is
| about I think the term is far more clear.
|
| > where the order matters
|
| I think you misunderstand the example. The article
| states:
|
| > Like before, *the order doesn't matter:* the client
| could begin a connection before the server starts
| accepting (the OS will buffer the client request in the
| meantime), or the server could start accepting first and
| wait for a bit before seeing an incoming connection.
|
| The one thing that must happen is that the server is
| running while the request is open. The server task must
| start and remain unfinished while the client task runs if
| the client task is to finish.
| Jtsummers wrote:
| I'm about to reply to the author because his article is
| actually confusing as written. He has contradictory
| definitions relative to his examples.
| didibus wrote:
| Not necessarily, but I guess it depends how you define
| "dependency".
|
| For example, it might be partial ordering is needed only,
| so B doesn't fully depend on A, but some parts of B must
| happen after some parts of A.
|
| It also doesn't imply necessarily that B is consuming an
| output from A.
|
| And so on.
|
| But there is a dependency yes, but it could be that the
| behavior of the system depends on both of them happening in
| some partial ordering.
|
| The difference is with asynchronous, the timing doesn't
| matter, just the partial or full ordering. So B can happen
| a year after A and it would eventually be correct, or at
| least within a timeout. Or in other words, it's okay if
| other things happen in between them.
|
| With synchronous, the timings tend to matter, they must
| happen one after the other without anything in-between. Or
| they might even need to happen together.
| nemothekid wrote:
| Consider: readA.await readB.await
|
| From the perspective of the application programmer, readA
| "block" readB. They aren't concurrent.
| join(readA, readB).await
|
| In this example, the two operations are interleaved and the
| reads happen concurrently. The author makes this distinction
| and I think it's a useful one, that I imagine most people are
| familiar with even if there is no name for it.
| sedatk wrote:
| I think that confuses paradigm with configuration. Say, if
| one thread waits on another to finish, that doesn't mean the
| code suddenly becomes "single-threaded", it just means your
| two threads are in a serialized configuration in that
| instance. Similarly, when async code becomes serialized, it
| doesn't cease to be async: the scaffolding to make it
| concurrent is there, it's just unused in that specific
| configuration.
|
| For example, C# uses this syntax: await
| readA(); await readB();
|
| when you have these two lines, the first I/O operation still
| yields control to a main executor during `await`, and other
| web requests can continue executing in the same thread while
| "readA()" is running. It's inherently concurrent, not in the
| scope of your two lines, but in the scope of your program.
|
| Is Zig any different?
| tossandthrow wrote:
| The author does not seem to have made any non-trivial projects
| with asynchronicity.
|
| All the pitfalls of concurrency are there - in particular when
| executing non-idempotent functions multiple times before previous
| executions finish, then you need mutexes!
| ajross wrote:
| > All the pitfalls of concurrency are there [in async APIs]
|
| This is one of those "in practice, theory and practice are
| different" situations.
|
| There is nothing in the async world that looks like a parallel
| race condition. Code runs to completion until it
| deterministically yields, 100% of the time, even if the
| location of those yields may be difficult to puzzle out.
|
| And so anyone who's ever had to debug and reason about a
| parallel race condition is basically laughing at that
| statement. It's just not the same.
| kibwen wrote:
| _> Code runs to completion until it deterministically yields_
|
| No, because async can be (quote often is) used to perform
| I/O, whose time to completion does not need to be
| deterministic or predictable. Selecting on multiple tasks and
| proceeding with the one that completes first is an entirely
| ordinary feature of async programming. And even if you don't
| _need_ to suffer the additional nondeterminism of your OS 's
| thread scheduler, there's nothing about async that says you
| can't use threads as part of its implementation.
| ajross wrote:
| And I repeat, if you think effects from unpredictable I/O
| completion order constitute equivalent debugging thought
| landscapes to hardware-parallel races, I can only laugh.
|
| Yes yes, in theory they're the same. That's the joke.
| saghm wrote:
| You're not reporting yourself, though, since you didn't
| make it clear at all in your initial comment you were
| talking about hardware. The previous comment you made
| only mentioned "parallel data races" in a conversation
| about software ecosystems, where both of those terms are
| regularly used to describe things that occur. You're
| laughing about dunking on people who you've run up to in
| the middle of a football field; no one stopped you from
| scoring because you didn't tell them that you're
| apparently playing an entirely different game on your
| own.
| ajross wrote:
| The term "hardware-parallel" was to clarify that I'm
| talking about genuine parallelism. SMP bugs are 100%
| software problems solved with software techniques. You
| learn about them in software courses in school. They're
| just much harder than getting your async rig to work.
| saghm wrote:
| You're probably right. I just don't think it was nearly
| as clear what you were talking about before as you seemed
| to be saying afterwards, so it came across as needlessly
| smug when clarifying like you did here would have been
| more than sufficient.
| tossandthrow wrote:
| Idk, I work in typescript though but I do have race
| conditions that look exactly like those of concurrent
| software.
|
| In particular promise.all([f,f,f]) where I want to ensure
| that I only Run the body of f a single time.
| alerighi wrote:
| You don't need mutex in async code, since there is no parallel
| execution whatsoever. In fact languages that use async
| programming as a first class citizen (JavaScript) don't even
| have a construct to do them.
|
| If you need to synchronize stuff in the program you can use
| normal plain variables, since it's guaranteed that your task
| will be never interrupted till you give control back to the
| scheduler by performing an await operation.
|
| In a way, async code can be used to implement mutex (or
| something similar) themself: it's a technique that I use often
| in JavaScript, to implement stuff that works like a mutex or a
| semaphores with just promises to syncronize stuff (e.g. you
| want to be sure that a function that itself does async
| operations inside is not interrupted, it's possible to do so
| with promises and normal JS variables).
| Spivak wrote:
| See this is what the OP is getting at, this is only true for
| async implementations that don't have any parallelism. That
| doesn't have to be the case, there's no reason that your
| javascript runtime couldn't take await
| foo() await bar()
|
| and execute them in two threads transparently for you. It
| just happens, like the Python GIL, that it doesn't. Your JS
| implementation actually already has mutexes because web
| workers with shared memory bring true parallelization along
| with the challenges that come with.
| Dylan16807 wrote:
| In the case of javascript, it's only allowed to do that
| when you can't detect it, situations where foo doesn't
| affect the output of bar. So _as far as pitfalls are
| concerned_ it does one thing at a time. The rest is a
| hidden implementation detail of the optimizer.
| mikojan wrote:
| https://developer.mozilla.org/en-
| US/docs/Web/API/LockManager...
|
| Please don't implement this yourself
| saghm wrote:
| > You don't need mutex in async code, since there is no
| parallel execution whatsoever. In fact languages that use
| async programming as a first class citizen (JavaScript) don't
| even have a construct to do them.
|
| This isn't even remotely true; plenty of languages have both
| async and concurrency, probably more than ones that don't. C#
| was the language that originated async/await, not JavaScript,
| and it certainly has concurrency, as do Swift, Python. Rust,
| and many more. You're conflating two independent proprieties
| of JavaScript as language and incorrectly inferring a link
| between them that doesn't actually exist.
| laserbeam wrote:
| Mutexes are part of the Io interface in zig. So is sleep,
| select, network calls, file io, cancellation...
| jayd16 wrote:
| I kind of think the author simply pulled the concept of yielding
| execution out of the definition of concurrency and into this new
| "asynchrony" term. Then they argued that the term is needed
| because without it the entire concept of concurrency is broken.
|
| Indeed so, but I would argue that concurrency makes little sense
| without the ability to yield and is therefore intrinsic to it.
| Its a very important concept but breaking it out into a new term
| adds confusion, instead of reducing it.
| LegionMammal978 wrote:
| I'd count pure one-to-one parallelism as a form of concurrency
| that doesn't involve any yielding. But otherwise, I agree that
| all forms of non-parallel concurrency have to be yielding
| execution at some cadence, even if it's at the instruction
| level. (E.g., in CUDA, diverging threads in a warp will
| interleave execution of their instructions, in case one branch
| tries blocking on the other.)
| kristoff_it wrote:
| >I kind of think the author simply pulled the concept of
| yielding execution out of the definition of concurrency and
| into this new "asynchrony" term.
|
| Quote from the article where the exact opposite is stated:
|
| > (and task switching is - by the definition I gave above - a
| concept specific to concurrency)
| jayd16 wrote:
| Well I'm having a hell of a time understanding what this
| article is trying to say. On a 3rd and 4th pass I think
| perhaps they mean task (in)dependency tracking is a
| fundamental concept. Independent tasks have "asynchrony."
| (Can we just say dependency and independency?)
|
| But even with that definition, it seems like the idea of
| promises, task tracking, etc is well tread territory.
|
| Then they conclude with how fire and forget tasks solve
| coloring but isn't that just the sync-over-async anti-
| pattern? I wouldn't be excited that my UI work stops to run
| something when there are no more green threads but they seem
| excited by it.
|
| Anyway, I guess I got too distracted by the high concept
| "this is a fundamental change in thinking" fluff of the
| article.
| omgJustTest wrote:
| Concurrency does not imply yielding...
|
| Synchronous logic does imply some syncing and yielding could be
| a way to sync - which is what i expect you mean.
|
| Asynchronous logic is concurrent without sync or yield.
|
| Concurrency and asynchronous logic do not exist - in real form
| - in von Neumann machines
| gf000 wrote:
| Could you expand on what you mean?
| kazinator wrote:
| Asynchrony, in this context, is an abstraction which separates
| the preparation and submission of a request from the collection
| of the result.
|
| The abstraction makes it possible to submit multiple requests and
| only then begin to inquire about their results.
|
| The abstraction allows for, but does not require, a concurrent
| implementation.
|
| However, the intent behind the abstraction is that there be
| concurrency. The motivation is to obtain certain benefits which
| will not be realized without concurrency.
|
| Some asynchronous abstractions cannot be implemented without some
| concurrency. Suppose the manner by which the requestor is
| informed about the completion of a request is not a blocking
| request on a completion queue, but a callback.
|
| Now, yes, a callback can be issued in the context of the
| requesting thread, so everything is single-threaded. But if the
| requesting thread holds a non-recursive mutex, that ruse will
| reveal itself by causing a deadlock.
|
| In other words, we can have an asynchronous request abstraction
| that positively will not work single threaded;
|
| 1 caller locks a mutex
|
| 2 caller submits request
|
| 3 caller unlocks mutex
|
| 4 completion callback occurs
|
| If step 2 generates a callback in the same thread, then step 3 is
| never reached.
|
| The implementation must use some minimal concurrency so that it
| has a thread waiting for 3 while allowing the requestor to reach
| that step.
| brabel wrote:
| Completely agree. The server/client example in the post was
| just one example of a program not being able to make progress,
| you've just gave another which cannot be solved the same way,
| and I would bet there are many more that they will be
| discovering over time. IMO when async is used, concurrency
| needs to be ensured.
| kazinator wrote:
| It depends on the API. If the only way to obtain the result
| of the asynchronous dispatch is a synchronous operation on a
| completion queue, then there are no cases where a single-
| threaded implementation will break, other than not providing
| the performance.
|
| However, we can argue that if there is only a synchronous
| operation to collect the result, then it's not truly async.
| Asynchrony should mean not only that we can initiate a
| request without waiting for the result, but that the
| completion can happen at any time.
| ang_cire wrote:
| > Asynchrony is not concurrency
|
| This is what I tell my boss when I miss standups.
| dvt wrote:
| "Asynchrony" is a very bad word for this and we already have a
| very well-defined mathematical one: commutativity. Some
| operations are commutative (order does not matter: addition,
| multiplication, etc.), while others are non-commutative (order
| does matter: subtraction, division, etc.). try
| io.asyncConcurrent(Server.accept, .{server, io});
| io.async(Cient.connect, .{client, io});
|
| Usually, ordering of operations in code is indicated by the line
| number (first line happens before the second line, and so on),
| but I understand that this might fly out the window in async
| code. So, my gut tells me this would be better achieved with the
| (shudder) `.then(...)` paradigm. It sucks, but better the devil
| you know than the devil you don't.
|
| As written, `asyncConcurrent(...)` is confusing as shit, and
| unless you memorize this blog post, you'll have no idea what this
| code means. I get that Zig (like Rust, which I really like fwiw)
| is trying all kinds of new hipster things, but half the time they
| just end up being unintuitive and confusing. Either implement
| (async-based) commutativity/operation ordering somehow (like
| Rust's lifetimes maybe?) or just use what people are already used
| to.
| ryandv wrote:
| Strictly speaking commutativity is defined over (binary)
| operations - so if one were to say that two async statements
| (e.g. connect/accept) are commutative, I would have to ask,
| "under what operation?"
|
| Currently my best answer for this is the bind (>>=) operator
| (including, incidentally, one of its instances, `.then(...)`),
| but this is just fuzzy intuition if anything at all.
| dvt wrote:
| Commutative operations (all of them I think?) are trivially
| generalized to n-ary operations (in fact, we do this via [?]
| and [?], in the case of addition and multiplication,
| respectively). You're right that the question of what
| "operation" we're dealing with here is a bit hazy; but I'd
| wager that it's probably in the family of the increment
| operation (N++ === N + 1 = 1 + N) since we're constantly
| evaluating the next line of code, like the head of a Turing
| machine.
|
| Edit: maybe it's actually implication? Since the previous
| line(s) logically imply the next. L_0 - L_1 - L_2 - L_n?
| Though this is non-commutative. Not sure, it's been a few
| years since my last metalogic class :P
| senderista wrote:
| Generalizing an associative binary op to an n-ary op just
| requires an identity element Id (which isn't always
| obvious, e.g. Id_AND=true but Id_OR=false).
| singularity2001 wrote:
| Identity is nop / pass
| JadeNB wrote:
| > Generalizing an associative binary op to an n-ary op
| just requires an identity element Id (which isn't always
| obvious, e.g. Id_AND=true but Id_OR=false).
|
| Only for n = 0, I think. Otherwise, generalizing
| associative binary f_2 to f_n for all positive integers n
| is easily done inductively by f_1(x) = x and f_{n +
| 1}(x_1, ..., x_n, x_{n + 1}) = f_2(f_n(x_1, ..., x_n),
| x_{n + 1}), with no need to refer to an identity. (In
| fact, the definition makes sense even if f_2 isn't
| associative, but is probably less useful because of the
| arbitrary choice to "bracket to the left.")
| dwattttt wrote:
| Implication sounds right. With no further analysis, running
| each line in order is correct (for whatever "order" is
| defined by a language, let's assume imperative).
|
| A compiler could recognise that e.g. L_2 doesn't depend on
| L_1, and would be free to reorder them. And compilers do
| recognise this in terms of data dependence of operations.
| benreesman wrote:
| It's a good intuition. This has been studied extensively, the
| composition rule that is lax enough to permit arbitrary
| effects but strict enough to guarantee this class of outcomes
| is (>>=). We can keep trying to cheat this as long as we
| want, but it's bind.
| xscott wrote:
| > "under what operation?"
|
| You could treat the semicolon as an operator, and just like
| multiplication over matrices, it's only commutative for a
| subset of the general type.
| ryandv wrote:
| Right, exactly. It's been said that (>>=) is a programmable
| semicolon.
|
| [0] https://news.ycombinator.com/item?id=21715426
| singularity2001 wrote:
| or carrots return/new line For that matter
| noduerme wrote:
| `.then()` is ugly, `await` is pretty, but wouldn't the
| critical part to guarantee commutivity less than guaranteed
| order (in js) be the `Promise.all([])` part?
| Nevermark wrote:
| The "operator" in this case would be the CPU executing 2 or N
| procedures (or functions).
|
| Commutivity is a very light weight pattern, and so is
| correctly applicable to many things, and at any level of
| operation, as long as the context is clear.
| jhanschoo wrote:
| Under function composition `;`, where both the LHS and RHS
| are viewed as functions operating on the whole environment
| state.
| ryandv wrote:
| Right; though it's a special kind of function composition
| (Kleisli composition) and often presented in a different
| form (bind, >>=).
| dooglius wrote:
| Commutativity is a much weaker claim because one is totally
| before or after the other. e.g. AB may commute with C so
| ABC=CAB but it is not necessarily the case that this equals
| ACB. With asynchrony you are guaranteed ABC=ACB=CAB. (There may
| be an exisiting mathematical term for this but I don't know it)
| dvt wrote:
| You can prove three-term commutativity from two-term (I did
| it years ago, I think it looked something like this[1]), so
| the ordering doesn't matter.
|
| [1] https://math.stackexchange.com/questions/785576/prove-
| the-co...
| Ar-Curunir wrote:
| Strictly speaking this also requires associativity.
| dooglius wrote:
| I'm not talking about a universe where all elements
| commute, I'm talking about a situation in which A, B, and C
| do not necessarily commute but (AB) and C do. For a
| rigorous definition: given X and Y from some semigroup G,
| say X and Y are asynchronous if for any finite
| decompositions X=Z_{a_1}Z_{a_2}...Z_{a_n} and
| Y=Z_{b_1}Z_{b_2}...Z_{b_m} (with Z's in G) then for any
| permutation c_1,...,c_{n+m} of a_1,...,a_n,b_1,...,b_m that
| preserves the ordering of a's and the ordering of the b's
| has XY=Z_{c_1}Z_{c_2}...Z_{c_{n+m}}. I make the following
| claim: if G is commutative then all elements are
| asynchronous, but for a noncommutative G there can exist
| elements X and Y that commute (i.e. XY=YX) but X and Y are
| not asynchronous.
| JW_00000 wrote:
| To give a concrete example, matrix multiplication is not
| commutative in general (AB [?] BA), but e.g.
| multiplication with the identity matrix is (AI = IA). So
| AIB = ABI [?] BAI.
|
| Or applied to the programming example, the statements:
| 1. Server.accept 2. Client.connect 3.
| File.write # write to completely unrelated file
|
| 123 = 312 [?] 321.
| jhanschoo wrote:
| I agree. T and U async with respect to each other means at
| least that T and U can be broken down into tasks t1, t2, t3,
| ... tn and u1, u2, ..., un, such that they can be interleaved
| in any order, but typically we still require that the t tasks
| are executed in sequential order. The divisions between the
| tasks are where they give up control, e.g. as they wait for
| data to be loaded into memory, or on a network call.
|
| This is still a special case of what we mean by async wrt
| each other, because depending on the interleaving at each
| step and e.g. the data loaded into memory, the number of
| tasks may change, but the idea is that they still eventually
| terminate in a correct state.
| brailsafe wrote:
| > "Asynchrony" is a very bad word for this and we already have
| a very well-defined mathematical one: commutativity.
|
| I don't think it's sufficient to say that just because another
| term defines this concept means it's a better or worse word.
| "commutativity" feels, sounds, and reads like a mess imo.
| Asynchrony is way easier on the palette
| throwawaymaths wrote:
| commutativity is also not correct, because 1) it means way
| more things than just temporal ordering and 2) there are
| cooky temporal ordering schemes you can come up with
| (interleaving multiple async/awaits in weird time-dependent
| ways) which aren't really describable in the simple
| mathematical notion of commutativity.
| hinkley wrote:
| Asynchrony also allows for partial ordering. Two operations may
| still need to be retired in a particular order without having
| to execute in that order.
|
| Subtraction for instance is not commutative. But you could
| calculate the balance and the deduction as two separate queries
| and then apply the results in the appropriate order.
| ordu wrote:
| _> As written, `asyncConcurrent(...)` is confusing as shit, and
| unless you memorize this blog post, you 'll have no idea what
| this code means. I get that Zig (like Rust, which I really like
| fwiw) is trying all kinds of new hipster things, but half the
| time they just end up being unintuitive and confusing. Either
| implement (async-based) commutativity/operation ordering
| somehow (like Rust's lifetimes maybe?) or just use what people
| are already used to._
|
| I can't agree. It is confusing, because you need to remember
| the blog post, it wouldn't be confusing in the slightest if you
| internalized the core idea. The question remains: is it worth
| it to internalize the idea? I don't know, but what I do know is
| some people will internalize it and try to do a lot of shit
| with this in mind, and after a while we will be able to see
| where this path leads to. At that point we will be able to
| decide if it is a good idea or not.
|
| _> "Asynchrony" is a very bad word for this and we already
| have a very well-defined mathematical one: commutativity._
|
| It is risky to use "commutativity" for this. Zig has operators,
| and some of them are commutative. And it will be confusing.
| Like if I wrote `f() + g(). Addition is commutative, then Zig
| is free to choose to run f() and g() in parallel. The order of
| execution and commutativity are different things. Probably one
| could tie them into one thing with commutative/non-commutative
| operators, but I'm not sure it is a good idea, and I'm sure
| that this is the completely different issue to experimenting
| with asynchrony.
| psychoslave wrote:
| I'm not sure they are that different, you could just as well
| store function calls in some constant each on its line then
| addition the result on a third. This is only syntax, not
| conceptual difference here. And on practical level, the
| difference is that the operator can be directly matched with
| some machine instruction, with operands being native data
| type such as integer.
|
| Still, you might then prefer a word as permutability, or
| swappability.
| delusional wrote:
| > Usually, ordering of operations in code is indicated by the
| line number
|
| Except for loops which allow going backwards, and procedures
| which allow temporarily jumping to some other locally linear
| operation.
|
| We have plenty of syntax for doing non-forwards things.
| tbrownaw wrote:
| > _Some operations are commutative (order does not matter:
| addition, multiplication, etc.)_
|
| Fun fact: order _does_ matter for addition. (When adding many
| floating-point numbers with widely varying exponents.)
| tsimionescu wrote:
| > So, my gut tells me this would be better achieved with the
| (shudder) `.then(...)` paradigm. It sucks, but better the devil
| you know than the devil you don't.
|
| The whole idea behind `await` is to make the old intuition work
| without the ugliness of `.then()`. `f(); await g(); h()` has
| exactly the expected execution ordering.
| Yoric wrote:
| Can confirm.
|
| In JS, we designed `await` specifically to hide `.then()`,
| just as we had designed `.then()` because callbacks made
| tracking control flow (in particular errors) too complex.
| psychoslave wrote:
| How is that any better to have await? Any resources I might
| consult on this?
| Yoric wrote:
| Well, one of the ways we "sold" async/await it to Google
| was by showing how we could improve Promise-based tests.
|
| I recall that one of our test suites was tens of
| thousands of lines of code using `then()`. The code was
| complicated enough that these lines were by and large
| considered write-only, partly because async loops were
| really annoying to write, partly because error-handling
| was non-trivial.
|
| I rewrote that test suite using `Task.spawn` (our
| prototype for async/await). I don't have the exact
| numbers in mind, but this decreased the number of LoC by
| a factor of 2-3 and suddenly people could see the
| familiar uses of loops and `try`/`catch`.
| tsimionescu wrote:
| Well, consider the difference between
| a().then(() => b()) .then(() =>
| c())
|
| Compared to await a() await b()
| await c()
|
| Even for this simple case I think it's much clearer. Then
| look at a more complex case: for( i=0;
| i<n; i++) { await a(i) ; }
|
| Now try re-writing this with then() and see the
| difference.
| psychoslave wrote:
| For the first one, it really feels like only a matter of
| how you break lines. Sure there is also the matter of
| anonymous function syntax, but
|
| The latter is more fair, here is a possible solution:
| const gen = (function* () { for (let i = 0; i <
| n; i++) yield a(i); })(); const run =
| next => !next.done && next.value.then(() =>
| run(gen.next())); run(gen.next());
|
| Or something similar using reduce. But in both cases, it
| illustrates the point, I guess.
|
| But if we are at point we can introduce new
| keywords/syntax in the language, it would just as well
| possible to come with something like
| a.chain(b, c)
|
| In case you need to pass parameters
| a.chain([b, p1, p2], c)
|
| And for the latter case const indexes =
| (function* () { for (let i = 0; i < n; i++) yield
| i; }) a.through(indexes)
| Yoric wrote:
| Well, sure, if you have `yield`, you pretty much have
| `await` already, as `await` is thin syntactic sugar on
| top of `yield` in all languages other than OCaml.
| alfiedotwtf wrote:
| > Usually, ordering of operations in code is indicated by the
| line number (first line happens before the second line, and so
| on), but I understand that this might fly out the window in
| async code
|
| This isn't always true at the language level, and almost
| certainly not at the CPU pipeline and microcode level.
|
| Logic languages like Prolog will execute statements out of
| order, by design. Other languages like Mercury use the IO monad
| to signify serial operations
| sdbrady wrote:
| I'm not sure what you mean by "statements" in Prolog as it's
| not a term the language defines. If you're referring to
| clauses, it's not true that execution is unordered: the
| Prolog interpreter attempts to unify a goal with clauses from
| the knowledge base in the order they appear. This ordering is
| semantically significant for control flow.
|
| If instead you're referring to goals within the body of a
| clause, this is also incorrect. Goals are evaluated strictly
| left-to-right, and each must succeed before the next is
| attempted. This evaluation order is likewise required and
| observable, especially in the presence of side effects.
| alfiedotwtf wrote:
| > the Prolog interpreter attempts to unify a goal with
| clauses from the knowledge base in the order they appear.
|
| I was under the impression that when plugging holes during
| unification, that these statements/clauses could happen in
| any order just as you would like solving a crossword puzzle
| skybrian wrote:
| The way I like to think about it is that libraries vary in which
| environments they support. Writing portable libraries that work
| in any environment is nice, but often unnecessary. Sometimes you
| don't care if your code works on Windows, or whether it works
| without green threads, or (in Rust) whether it works without the
| standard library.
|
| So I think it's nice when type systems let you declare the
| environments a function supports. This would catch mistakes where
| you call a less-portable function in a portable library; you'd
| get a compile error, indicating that you need to detect that
| situation and call the function conditionally, with a fallback.
| andrewstuart wrote:
| That's word games.
|
| If I launch 2 network requests from my async JavaScript and both
| are in flight then that's concurrent.
|
| Definition from Oxford Dictionary adjective 1. existing,
| happening, or done at the same time. "there are three concurrent
| art fairs around the city"
| necovek wrote:
| It is an attempt to delineate some classes of concurrent &
| parallel programming challenges using a new term (really,
| redefining it in the programming context to be a bit tighter).
|
| Oxford dictionary holds no relevance here, unless it has took
| over a definition from the field already (eg. look up "file": I
| am guessing it will have a computer file defined there) -- but
| as it lags by default, it can't have specific definitions being
| offered.
| jillesvangurp wrote:
| The concepts of concurrency and parallelism are adjacent enough
| that they are often confused. A lot of languages provide basic
| concepts for both but use different frameworks for both. So the
| difference really matters in that case. Or the frameworks are
| just a bit low level and the difference really matters for that
| reason (because you need to think about and be aware of
| different issues).
|
| I've been using Kotlin in the last few years. And while it is
| not without issues, their co-routines approach is a thing of
| beauty as it covers the whole of this space with one framework
| that is designed to do all of it and pretty well thought
| through. It provides a higher level approach in the form of
| structured concurrency, which is what Zig is dancing around
| here if I read this correctly (not that familiar with it so
| please correct if wrong) and not something that a lot of
| languages provide currently (Java, Javascript, Go, Rust,
| Python, etc.). Several of those have work in progress related
| to that though. I could see python going there now that they've
| bit the bullet with removing the GIL. But they have a bit of
| catching up to do. And several other languages provide ways
| that are similarly nice and sophisticated; and some might claim
| better.
|
| In Kotlin, something being async or not is called suspending.
| Suspending just means that "this function sometimes releases
| control back to whatever called it". Typical moments when that
| happens are when it does evented IO and/or when it calls into
| other suspending functions.
|
| What makes it structured concurrency is that suspend functions
| are executed in a scope, which has something called a
| dispatcher and a context (meta data about the scope). Kotlin
| enforces this via colored "suspend" functions. Calling them
| outside a coroutine scope is a compile error. Function colors
| are controversial with some. But they works and it's simple
| enough to understand. There's zero confusion on the topic.
| You'll know when you do it wrong.
|
| Some dispatchers are single threaded, some dispatchers are
| threaded, and some dispatchers are green threaded (e.g. if on
| the JVM). In Kotlin, a coroutine scope is obtained with a
| function that takes a block as a parameter. That block receives
| its scope as a context parameter (typically 'this'). When the
| block exits, the whole tree of sub coroutines the scope had is
| guaranteed to have completed or failed. The whole tree is
| cancelled in case of an exception. Cancellation is one of the
| nasty things many other languages don't handle very well. A
| scope failure is a simple exception and if something cancelled,
| that's a CancellationException. If this sounds complicated,
| it's not that bad (because of Kotlin's DSL features). But
| consider it necessary complexity. Because there is a very
| material difference between how different dispatchers work.
| Kotlin makes that explicit. But otherwise, it kind of is all
| the same.
|
| If inside a coroutine, you want to do two things
| asynchronously, you simply call functions like launch or async
| with another block. Those functions are provided by the
| coroutine scope. If you don't have one, you can't call those.
| That block will be executed by a dispatcher. If you want use
| different threads, you give async/launch an optional new
| coroutine scope with it's own dispatcher and context as a
| parameter (you can actually combine these with a + operator).
| If you don't provide the optional parameter, it simply uses the
| parent scope to construct a new scope on the fly. Structured
| concurrency here means that you have a nested tree of
| coroutines that each have their own context and dispatchers.
|
| A dispatcher can be multi threaded (each coroutine gets its own
| thread) and backed by a thread pool, or a simple single
| threaded dispatcher that just lets each coroutine run until it
| suspends and then switches to the next. And if you are on the
| JVM where green thread pools look just like regular thread
| pools (this is by design), you can trivially create a green
| thread pool dispatcher and dispatch your co routines to a green
| thread. Note, this is only useful when calling into Java's
| blocking IO frameworks that have been adapted to sort of work
| with green threads (lots of hairy exceptions to that).
| Technically, green threads have a bit more overhead for context
| switching than Kotlin's own co-routine dispatcher. So use those
| if you need it; avoid otherwise unless you want your code to
| run slower.
|
| There's a lot more to this of course but the point here is that
| the resulting code looks very similar regardless of what
| dispatchers you use. Whether you are doing things concurrently
| or in parallel. The paradigm here is that it is all suspend
| functions all the way down and that there is no conceptual
| difference. If you want to fork and join coroutines, you use
| functions like async and launch that return jobs that you can
| await. You can map a list of things to async jobs and then call
| awaitAll on the resulting list. That just suspends the parent
| coroutine until the jobs have completed. Works exactly the same
| with 1 thread or a million threads.
|
| If you want to share data between your co-routines, you still
| need to worry about concurrency issues and use locks/mutexes,
| etc. But if your coroutine doesn't do that and simply returns a
| value without having side effects on memory (think functional
| programming here), things are quite naturally thread safe and
| composable for structured concurrency.
|
| There are a lot of valid criticisms on this approach. Colored
| functions are controversial. Which I think is valid but not as
| big of a deal in Kotlin as it is made out to be. Go's approach
| is simpler but at the price of not dealing with failures and
| cancellation as nicely. All functions are the same color. But
| that simplicity has a price (e.g. no structured concurrency).
| And it kind of shovels paralellism under the carpet. And it
| kind of forces a lot of boiler plate on users by not having
| proper exceptions and job cancellation mechanisms. Failures are
| messy. It's simple. But at a price.
| necovek wrote:
| The idea behind Zig approach which is only being implemented
| is to extend concurrent code to be executable outside the
| coroutine context, making it trivially serializable when
| that's possible (thus the networking example).
|
| In general, the heavy lifting should always be moved to the
| lower level infrastructure (compiler, standard library, RDBMS
| system in case of ACID guarantees...) -- leaving developer
| his brainspace for the business logic.
|
| This requires minimum "function coloring", and I'd prefer if
| Python took that approach instead.
| messe wrote:
| > If I launch 2 network requests from my async JavaScript and
| both are in flight then that's concurrent.
|
| That's because JS conflates the two. The async keyword in
| JavaScript queues things for the event loop which is running in
| a different thread, and progress will be made on them even if
| they are never awaited. In Rust, for example, nothing will
| happen _unless_ those Futures are awaited.
| tekbog wrote:
| There's a great old book on this if someone wants to check it:
| Communicating Sequential Processes. From Hoare. Go channels and
| the concurrent approach was inspired on this.
|
| I also wrote a blog post a while back when I did a talk at work,
| it's Go focused but still worth the read I think.
|
| [0] https://bognov.tech/communicating-sequential-processes-in-
| go...
| bentleya wrote:
| Non-blocking i/o isn't asynchrony and the author should know
| better. Non-blocking io is a building block of asynchronous
| systems -- it is not asynchony itself. Today's asynchronous
| programming did not exist when non-blocking I/O was implemented
| in Unix in the 80's.
| nemothekid wrote:
| I think I'm missing something here, but the most interesting
| piece here is how would stackless coroutines work in Zig?
|
| Since any function can be turned into a coroutine, is the
| red/blue problem being moved into the compiler? If I call:
| io.async(saveFileA, .{io});
|
| Is that a function call? Or is that some "struct" that gets
| allocated on the stack and passed into an event loop?
|
| Furthermore, I guess if you are dealing with pure zig, then its
| fine, but if you use any FFI, you can potentially end up issuing
| a blocking syscall anyways.
| laserbeam wrote:
| I hope this is not a bad answer as I tried to understand what
| stackless coroutines even are for the past week.
|
| 1. Zig plans to annotate the maximum possible stack size of a
| function call https://github.com/ziglang/zig/issues/23367 . As
| people say, this would give the compiler enough information to
| implemented stackless coroutines. I do not understand well
| enough why that's the case.
|
| 2. Allegedly, this is only possible because zig uses a single
| compilation unit. You are very rarely dealing with modules that
| are compiled independently. If a function in zig is not called,
| it's not compiled. I can see how this helps with point 1.
|
| 3. Across FFI boundaries this is a problem in every language.
| In theory you can always do dumb things after calling into a
| shared library. A random C lib can always spawn threads and do
| things the caller isn't expecting. You need unsafe blocks in
| rust for the same reason.
|
| 4. In theory, zig controls the C std library when compiling C
| code. In some cases, if there's only one Io implementation used
| for example, zig could replace functions in the c std library
| to use that io vtable instead.
|
| Regardless, I kinda wish kristoff/andrew went over what
| stackless coroutines are (for dummies) in an article at some
| point. I am unsure people are talking about the same thing when
| mentioning that term. I am happy to wait for that article until
| zig tries to implement that using the new async model.
| kibwen wrote:
| _> As people say, this would give the compiler enough
| information to implemented stackless coroutines._
|
| Stackless coroutines require creating a structure big enough
| to hold all the locals for the would-be function, but Zig
| already has that information, along with Rust, and, by
| definition, every other language that already supports
| stackless coroutines.
|
| The root of that linked issue is that Zig has some desire to
| statically compute the _total_ stack usage of an entire
| program, but the difficulty there is not in computing the
| stack size for any given function (which is generally trivial
| in most languages; supporting growable stack via something
| like C 's `alloca` is the exception, not the rule). The
| difficulty is that recursive functions can push an unbounded
| number of function calls to the stack. So what Zig wants to
| do is forbid recursion, even mutual recursion, unless you do
| some kind of opt-in.
|
| And this is where "Allegedly, this is only possible because
| zig uses a single compilation unit" comes in, because
| detecting mutual recursion is tricky, especially when virtual
| dispatch gets involved.
|
| But no, you don't need Zig-style whole-program compilation to
| make that happen. All you need is 1) to be able to detect
| mutual recursion within a single compilation unit (again,
| stymied by virtual dispatch), and then 2) to prevent cyclical
| dependencies between compilation units. Go and Rust both do
| the latter, so they could get away with the same analysis,
| assuming you can find a good solution for the former.
| User23 wrote:
| Asynchrony, parallelism, concurrency, and even deterministic
| execution (albeit as a degenerate case) are all just species of
| nondeterminism. Dijkstra and Scholten's work on the subject is
| sadly under appreciated. And lest one thing this was ivory tower
| stuff, before he was a professor Dijkstra was a systems engineer
| writing operating systems on hilariously bad, by our standards,
| hardware.
| riwsky wrote:
| "Permission for concurrency is not an obligation for concurrency.
| Zig lets you explicitly permit-without-obligation, to support the
| design of libraries that are polymorphic over a/sync 'function
| color'."
| Salgat wrote:
| It's more accurate to say that callbacks and async/await can
| facilitate concurrency.
| reichstein wrote:
| So "cooperative multitasking is not preemptive multitasking".
|
| The typical use of the word "asynchronous" means that the
| _language is single-threaded_ with cooperative multitasking
| (yield points) and event based, and external computations may run
| concurrently, instead of blocking, and will report result(s) as
| events.
|
| There is no point in having asynchrony in a multithreaded or
| concurrent execution model, you can use blocking I/O and still
| have progress in the program while that one execution thread is
| blocked. Then you don't need the yield points to be explicit.
| Yoric wrote:
| While this is indeed the most common use, I'll bring as
| counter-examples Rust (or C#, or F#, or OCaml 5+) that supports
| both OS threads and async. OS threads are good for CPU-bound
| tasks, async for IO-bound tasks.
|
| The main benefit of having async (or Go-style M:N scheduling)
| is that you can afford to launch as many
| tasks/fibers/goroutines/... as you want, as long as you have
| RAM. If you're using OS threads, you need to pool them
| responsively to avoid choking your CPU with context-switches,
| running out of OS threads, running out of RAM, etc. - hardly
| impossible, but if you're doing more than just I/O, you can run
| into interesting deadlocks.
| wasmperson wrote:
| > The main benefit of having async (or Go-style M:N
| scheduling) is that you can afford to launch as many
| tasks/fibers/goroutines/... as you want
|
| Some have argued that the real solution to this problem is to
| "just" fix OS threads. Rumor has it Google has done exactly
| this, but keeps it close to their chest:
|
| https://www.youtube.com/watch?v=KXuZi9aeGTw
|
| https://lwn.net/Articles/879398/
|
| Somewhat related and also by Google is WebAssembly Promise
| Integration, which converts blocking code into non-blocking
| code without requiring language support:
|
| https://v8.dev/blog/jspi
|
| I see a possible future where the "async/await" idea simply
| fades away outside of niche use-cases.
| Yoric wrote:
| I think that PI will have a role, but I suspect that it can
| quickly destroy the performance of wasm code in unexpected
| ways.
|
| As for fixing OS threads, indeed, this may very well change
| the ecosystem, but many developers expect their code to be
| cross-platform, so it might take a while before there is a
| solution that works everywhere.
| Yoric wrote:
| Defining async is hard. And I'm writing this as one of the many
| people who designed async in JavaScript.
|
| I don't quite agree with the definition in this post: just
| because it's async doesn't mean that it's correct. You can get
| all sorts of user-land race conditions with async code, whether
| it uses `async`/`await` (in languages that need/support it) or
| not.
|
| My latest formulation (and I think that it still needs work) is
| that async means that the code is explicitly structured for
| concurrency.
|
| I wrote some more about the topic recently:
| https://yoric.github.io/post/quite-a-few-words-about-async/ .
| atoav wrote:
| I am not deep into the matter but I would have given the
| answer: Async code is making code that would have been blocking
| non-blocking in a manner other stuff can still happen while it
| is being completed.
|
| Since I work a lot in embedded loops where long running
| blocking snippets could literally break your I/O, lead to
| visible/audible dropouts etc. this would be the obvious answer.
| Yoric wrote:
| But that's the thing: async, by itself, doesn't guarantee
| that anything is non-blocking. For your fiber (or any other
| kind of user-land abstraction) to be non-blocking, you MUST
| ensure that it doesn't perform any blocking call.
|
| All async does is give you (some of) the tools to make code
| non-blocking.
| atoav wrote:
| Yeah, sure I mean in embedded-land any async snippet could
| perform any number of things, like firing a delay command
| that puts the whole processor to sleep.
|
| This could potentially be avoided by clever enough
| compilers or runtimes, but I am not sure whether that would
| really be benefitial.
|
| I am a fan of making things explicit, so the closer peoples
| idea of what aync is and what it isn't matches reality the
| better. Alternatively we should get the definition of what
| async _should be_ clear first and then make the adjustment
| to the abstractions so they give us the guarantees people
| would natuarally assume come with that.
| Yoric wrote:
| Yeah, I'm insisting because I recently reviewed a PR with
| async code calling blocking code, which made the entire
| exercise pointless. And that was from an experienced dev.
|
| There used to be a few compilers that used static
| analysis to predict the cost of a call (where the cost of
| I/O was effectively considered infinite) and in which you
| could enforce that a branch only had a budget of N.
| Modern architectures tend to mess up with any finite
| value of N, but you could fairly easily adapt such
| techniques to detect unbounded values.
| YZF wrote:
| The entire point might be to offload the blocking call
| and do something else while it's blocking.
|
| There's a style of "asynchronous programming" where
| everything is designed to be non-blocking and there can
| be asynchronous programming with blocking code. In fact
| the first style can be emulated by offloading every
| blocking call to a different thread/greenthread/fiber and
| that's basically what's happening under the hood unless
| there is some fundamental support for non-blocking at the
| lower levels (sometimes all the way down to the
| hardware).
| quintu5 wrote:
| _cries in Python asyncio_
| Yoric wrote:
| _sympathizes_
|
| If you look on the bright side, it looks like free-
| threading is approaching, and OCaml has demonstrated how,
| by removing the GIL and adding exactly one primitive, you
| can turn a powerful enough language into a
| concurrency/parallelism powerhouse with minimal user-
| visible changes!
| YZF wrote:
| You can run blocking code asynchronously from other code. I
| think the parent means something like if you have a
| blocking operation (function call e.g.) on a thread you can
| create another thread and run that blocking operation
| there, magically transforming a blocking call to something
| that looks like a non-blocking call (thus allowing other
| code to do something else instead of waiting)
| twwwt wrote:
| The important thing to me is to distinguish between the
| abstract concept of asynchronism and how it can be implemented,
| and by the latter I mean both at the abstract level of a
| programming language and by technical coordination means in a
| machine. For the abstract concept at the highest level, well,
| it is just the dual of synchronism: two (or more) parties that
| somehow need to work together (i.e., one has a dependency on
| the other in a way that certain things need to happen _before_
| another one can continue) are not synchronized, meaning that it
| is not known or not defined _when_ the things that need to
| happen _after_ something else will be done. Seen that way, this
| definition is not hard. The hard thing can be the abstract
| means designed in a language: the amount of cognitive effort it
| takes in order to comprehended them and /or use them (in an
| fault-free way).
| Yoric wrote:
| Largely agreed: async means that the code is structured in
| such a manner that the only way to be certain that a task is
| complete is to perform some rendez-vous. By extension, it
| covers mechanisms to make this happen in your code.
| rasmul wrote:
| i am not even sure we should define async
|
| it may be hard (it is) because it cannot be matched to one
| thing
|
| the question is: is it useful to define async? or event loop?
| there must be tons of concepts i have no idea in the realm of
| physical chips that make true parallelism possible
|
| i am totally fine with "user finger" and "quickies", job queues
| and blocking or non-blocking APIs
|
| the finger symbolizes touch events and even mouse clicks and
| keyboard or general user initiated events, which i have to
| match to quickies which are very tiny (execution time)
| blocking(!) jobs that will be queued by the browser
|
| to reach my goals, i prefer non-blocking APIs because i can
| discard some time consuming jobs to underlying systems and i
| jsut write a quicky for what i want (store data in indexed db)
| and what will happen if it succeeds or fails etc (different
| quickies)
|
| sync, async do not really help me, of course i have to
| understand when others talk about it or i see or use (or my
| preferred AI coder) async, but it just means non-blocking API
|
| but again, the async programming model is actually writing very
| much blocking quickies where the non-blocking nature is the
| small, atomic nature (execution-time-wise) of the blocking jobs
| I try to match to chaotic, non-deterministic events, triggered
| by fingers or browsers or whatever
|
| I actually dont care, just hope that the browser code uses
| great concurrent models with cpp or rust or whatever and the
| device has multiple executions units (os threads) and the os
| scheduler does a great job, managing things whether there are
| more execution units or just 1 available
|
| async for me a not well defined concept and even if it was
| somehow defined, i am not sure it would be useful to me
|
| useful concepts are events, the blocking nature of jobs i write
| in js, what my functions see (closure i guess), what runs as a
| job if i use APIs and what runs as a different job after the
| events the browser triggers (ready, error whatever)
|
| even the name callback was extremely confusing because i really
| thought back then that the code somehow stops there and waits
| for a callback... no, it runs to the end of that section and
| you really have to understand what other things run when it
| "calls back" and what that code sees
|
| to be honest, i think it is a mess and genius at the same
| time... but understanding "async" or rather the model was
| really difficult because i just dont think this means anything
|
| it is actually very simple to understand with different
| concepts like event, blocking job, job queue, non-blocking API
|
| what i also find important to know what we are doing and what
| others do like browser code, os etc... it is a bit like a cpp
| code declares a concurrent model with a thrad but the os will
| decide... in js, we use non-blocking api which implicitly
| declares a probably concurrent model the browser or node or
| whatever should use and i am sure they always do
|
| the most important thing is to keep your jobs quick, probably
| under 30-50ms and non-blocking API are great because your job
| just declares the intent and done, and languages like cpp, rust
| will declare the os that they want the actual task done
| concurrently so even if the os has one real physical thread,
| the UI will be responsive since the OS will switch between UI
| code execution and "real task" execution like some networking
| or database or whatever
|
| but all an "async" programmer has to do is to create a great UX
| model and match events to quickies
| Yoric wrote:
| Not entirely sure what you mean.
|
| If you're writing that you don't need to understand how your
| browser works, "just" to make things fast enough... well,
| sure, go ahead.
|
| But anybody who wants to graduate to a higher-level of
| comprehension, will need to understand a bit better under the
| hood.
| jongjong wrote:
| I think there's not much point trying to define these concepts as
| there is no consensus about what they mean. Different people have
| clear ideas about what each concept means but they just don't
| agree.
|
| It's like integration tests vs unit tests... Most developers
| think they have a clear idea about what each one means, but based
| on my experience there is very little consensus about where the
| line is between unit test vs integration test. Some people will
| say a unit test requires mocking or stubbing out all
| dependencies, others will say that this isn't necessary; so long
| as you mock out I/O calls... Others will say unit tests can make
| I/O calls but not database calls or calls which interface with an
| external service... Some people will say that if a test covers
| the module without mocking out I/O calls then it's not an
| integration test, it's an end-to-end test.
|
| Anyway it's the same thing with asynchrony vs concurrency vs
| parallelism.
|
| I think most people will agree that concurrency can potentially
| be achieved without parallelism and without asynchrony. For many
| people, asynchrony has the connotation that it's happening in the
| same process and thread (same CPU core). Some people who work
| with higher level languages might say that asynchrony is a kind
| of context switching (as it's switching context in the stack when
| callbacks at called or promises resolved) but system devs will
| say that context switching is more granular than that and not
| constrained to the duration of specific operations, they'll say
| it's a CPU level concept.
| t43562 wrote:
| I think it's a great idea to not have to have two libraries - so
| its a "tick" from me for any idea that permits it.
|
| The thing that bothers me in general about asynchronous code is
| how you test it so that you know with some confidence that if it
| passes the tests today you have replicated all the
| scenarios/orderings that might happen in production.
|
| You have this same problem with threads of course and I've always
| found multithreaded programs to be much harder to write and
| debug....such that I personally use threading only when I feel I
| have to.
|
| The actual problem with it is that caution is communicating it to
| developers. I recently had to work on a python system where the
| developers were obviously doing Javascript half the time. So ...
| hooray.... they put out a huge changeset to make the thing
| async....and threaded. Oddly enough none of them had ever heard
| of the GIL and I got the feeling of being seen as an irritating
| old bastard as I explained it to their blank stares. Didn't
| matter. Threading is good. Then I pointed out that their tests
| were now always passing no matter if they broke the code. Blank
| stares. They didn't realise that mangum forced all background
| tasks and async things to finish at the end of an HTTP request so
| their efforts to shift processing to speed up the response were
| for nothing.
|
| Knowing things doesn't always matter if you cannot get other
| people to see them.
| Tanjim wrote:
| That resonates. Testing asynchronous and multithreaded code for
| all possible interleavings is notoriously difficult. Even with
| advanced fuzzers or concurrency testing frameworks, you rarely
| gain full confidence without painful production learnings.
|
| In distributed systems, it gets worse. For example, when
| designing webhook delivery infrastructure, you're not just
| dealing with async code within your service but also network
| retries, timeouts, and partial failures across systems. We ran
| into this when building reliable webhook pipelines; ensuring
| retries, deduplication, and idempotency under high concurrency
| became a full engineering problem in itself.
|
| That's why many teams now offload this to specialized services
| like Vartiq.com (I'm working here), which handles guaranteed
| webhook delivery with automatic retries and observability out
| of the box. It doesn't eliminate the async testing problem
| within your own code, but it reduces the blast radius by
| abstracting away a chunk of operational concurrency complexity.
|
| Totally agree though - async, threading, and distributed
| concurrency all amplify each other's risks. Communication and
| system design caution matter more than any syntax or library
| choice.
| Tainnor wrote:
| > That's why many teams now offload this to specialized
| services like Vartiq.com
|
| It would be nice to add a disclaimer that this is a system
| you're working on.
| Tanjim wrote:
| Thanks for pointing out. Edited. :)
| kristoff_it wrote:
| We plan to have in Zig a testing `Io` implementation that will
| potentially use fuzzing to stress test your code under a
| concurrent execution model.
|
| That said, I think a key insight is that we expect most of the
| library code out there to not do _any_ calls to `io.async` or
| `io.asyncConcurrent`. Most database libraries for example don
| 't need any of this and will still contain simple synchronous
| code. But then that code will be able to be used by application
| developers to express asynchrony at a higher level:
| io.async(writeToDb) io.async(doOtherThing)
|
| Which makes things way less error prone and simpler to
| understand than having async/await sprinkled all over the
| place.
| lll-o-lll wrote:
| More powerful than a "fuzzing" test io would be a
| deterministic test io. I.e., one you can tick forward the
| various concurrent branches deterministically to prove that
| various races are safely handled. This makes it possible to
| capture all those "what if thread A executes this line first
| then B is executed" etc. Something that is missing in most
| concurrent frameworks.
| ddingus wrote:
| I do not see these definitions as quite right:
|
| >Asynchrony: the possibility for tasks to run out of order and
| still be correct.
|
| I like this. Great addition and yes it was missing.
|
| >Concurrency: the ability of a system to progress multiple tasks
| at a time, be it via parallelism or task switching.
|
| I would say here, be it multiprocessing or task switching.
|
| >Parallelism: the ability of a system to execute more than one
| task simultaneously at the physical level.
|
| This is technically multiprocessing as expressed above.
|
| So, what is the difference between parallelism and concurrency?
|
| Parallel tasks are like shaders. It is the same task, running
| many instances at the same time at the physical layer.
|
| GPU devices are capable of parallel computing, for example.
|
| Concurrent tasks are different tasks running at the same time at
| the physical layer. Often, the data is different too. Say a
| sprite engine running at the same time as a video display driver
| on the physical layer.
|
| The shaders can all be running the same code but are processing
| different data elements, say each pixel having a position and is
| part of a larger rendering.
|
| A GPU is a massively parallel multiprocessor.
|
| A Threadripper is a massive Concurrent multiprocessor. It can
| also perform as a modest parallel multiprocessor.
|
| The difference lies in what the various compute units can do and
| what they are actually doing.
|
| Put another way, a 10ghz single core CPU is not a multiprocessor.
| It performs sequential computing and it can task switch to handle
| the same task load as a lower clock rate multiprocessor would
| handle.
|
| A 10ghz multi core CPU is a concurrent multiprocessor, but is not
| a GPU. It could run shaders on par with a lower clock GPU. BUT a
| lower clock GPU cannot run a variety of tasks in the same way.
| throwup238 wrote:
| _> Parallel tasks are like shaders. It is the same task,
| running many instances at the same time at the physical layer._
|
| That's single instruction multiple data which I would argue is
| an orthogonal concern. A better example of parallelism would be
| FPGAs. All of the gates are switching all at the same time* and
| you have to actually figure out how to synchronize the whole
| lot to get anything useful out of them.
|
| * PLL notwithstanding
| ddingus wrote:
| Yes, SIMD is a form of parallel computing. But that is not
| the only form. A shader is one, and the difference is lots of
| instructions and lots of data. It is just the same
| instructions executing in all the instances.
|
| Concurrent would be many tasks running at the same time with
| each task containing different jnstructions on either the
| same data, or different data.
| gf000 wrote:
| A single-threaded CPU can do concurrent processing just fine -
| this is an important distinction compared to parallelism, and I
| think you are muddying the two up a bit here.
| yazantapuz wrote:
| I allways think of async concurrency as state machines.
| jlouis wrote:
| A good way to gauge if "asynchrony" is a term we need is to test
| if it is useful in other contexts than just a single language, or
| a single concurrency design.
|
| If it's needed to reason correctly in a wide set of concurrency
| models, then I'd say it's going to be a useful addition. If not,
| then I'd say it's not really worth using in the grander scheme of
| things.
|
| I.e., does this make any sense in Haskell, Erlang, OCaml, Scheme,
| Rust, Go, .... ?(assuming we pick one of the many concurrency
| models available in Haskell, Rust and OCaml).
|
| More generally: if things are cooperatively scheduled, then
| there's a need for attention to additional details. This is
| because it's much easier for a bad piece of code to affect the
| system as a whole, by locking it up, or generating latency-
| problems. In a preemptively scheduled world, a large group of
| problems disappear instantly, since you can't lock up the system
| in the same way.
| northhnbesthn wrote:
| As a network programmer who has written a metric asston of
| concurrent parallel asynchronous whatever-the-hell code [0], this
| article is confusing. It seems like doing somersaults over a
| leaky abstraction. The tool for the job is wrong and its
| implementation is wrong if you can shit the bed this easily.
|
| [0] debugging is fun precisely because it's amusing to watch
| people be frightened of having to debug multi threaded hydras.
| lowbloodsugar wrote:
| >Asynchrony: the possibility for tasks to run out of order and
| still be correct.
|
| This is simply wrong. Asynchrony makes no claims about two
| unrelated tasks, so "order" here is irrelevant and in terms of
| each task, again, we expect the executions of a given task to be
| in order. So this statement cannot be true under either
| interpretation.
|
| There's lots more wrong with the article as you would expect when
| the starting premise is wrong.
___________________________________________________________________
(page generated 2025-07-19 23:02 UTC)