Live data from Hacker News

Asynchrony is not concurrency

kristoff.it

151–160 of 228 posts

Re: Asynchrony is not concurrency

#151
post #25
post #9

Earlier quoted context omitted.

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.

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.

[deleted]

Re: Asynchrony is not concurrency

#152
post #95

Earlier quoted context omitted.

Rust does this, if you don’t call await on them. You can then await on the join of both.

Is the "join" syntax part of the language?

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

Re: Asynchrony is not concurrency

#153
post #87

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

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

Re: Asynchrony is not concurrency

#154
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.

Re: Asynchrony is not concurrency

#155

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 motivati…

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.

Re: Asynchrony is not concurrency

#156

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"

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.

Re: Asynchrony is not concurrency

#157
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/ .

Re: Asynchrony is not concurrency

#158
post #105

Earlier quoted context omitted.

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

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,..…

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.

Re: Asynchrony is not concurrency

#159
post #87

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

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

Re: Asynchrony is not concurrency

#160
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.

Post reply on HN