lainchan / λ
Asynchronous Programming: Paradigms and Implementations
Pris · 2021-09-26 16:18
> Reactive Programming
https://en.wikipedia.org/wiki/Reactive_programming
https://blog.oakbits.com/introduction-to-rxpy.html
https://projectreactor.io/
> Futures/Promises
https://en.wikipedia.org/wiki/Futures_and_promises
https://blog.domenic.me/youre-missing-the-point-of-promises/
> Async I/O
aio - https://manpages.debian.org/bullseye/manpages/aio.7.en.html
io_uring - https://manpages.debian.org/experimental/liburing-dev/io_uring.7.en.html
To start some discussion:
What do you like/dislike about current async programming?
What are things you think it does really well, and really poorly?
Do you think async should be the default mode of operation for stuff like services and I/O? If so, what cases do you think async does not belong?
I'll start off: my work has started using reactive programming a few months ago, and is still exploring proper micro-patterns for working in it. I like the FP aspect, however the library I've been using forces me to translate thrown exceptions into error values, and if I don't, the exception is no longer catchable in the entire reactive chain.
Spooky · 2021-09-27 00:14
here is a good and somewhat gentle introduction to the topic of io_uring that helped me understand it a little better: https://unixism.net/loti/index.html
synchronized · 2022-02-10 07:35
bump, this is an interesting topic
I tried reactor/spring webflux at my job and got my ass kicked by it
thoughts on webflux and its future? A lot of companies in my country, mainly finace-relared ones, seem to be adopting it now
string · 2022-02-10 12:36
I like Tcl's event loop and I hate how complicated conditions variables (incl. the spurrious wakeup thing) are in C/C++; and Bordeaux-Threads inherits this soykaf.
Yuki Nagato · 2022-02-10 13:33
I'm embarking in a project that will (most likely) make use of asynchronous programming. First time I ever try it in a project. The tldr is that I'll be programming a drone to move while processing images that it'll capture underneath it and to respond according to what it sees. Limited in resources so I'll probably have to turn to async + multi-threading if possible.
>>30064
> I hate how complicated conditions variables (incl. the spurrious wakeup thing) are in C/C++
What kind of problems did you run into?
Rick Deckard · 2022-02-10 18:29
in JavaScript I always get confused how objects are passed around and callbacks.
t. bad programmer
John Titor · 2022-02-11 00:32
>>30065
None, you just need a lot of boilerplate to make it work. See the example at pthread_cond_wait(3).
Drifter · 2022-02-28 11:27
Any advice for learning async IO in Python?
I'm writing an application that interacts with a WebSocket API and I want to make it async.
Wintermute · 2022-03-10 21:19
>>27681
Actor Model. What Elixir and Erlang do.
Other languages have implementations of it too.
https://en.wikipedia.org/wiki/Actor_model
Modus · 2022-10-16 07:08
>>27681
I recently came came across this presentation by one if the developers of the boost library. I thought he explained some challenges and solutions of writing parallel code quite well. Particularly, learning about the concept of futures, promises and dynamic work scheduling.
I assume these concepts will not be new to anyone who is already a little knowledgeable about parallelism. But to beginner like me, I thought it was well explained, so I'll leave it here in case anyone is interested.
https://youtu.be/js-e8xAMd1s
H.R. Giger · 2023-03-10 06:27
>>30379
I would recommend CSP instead of actors if it's not distributed across multiple computers.
Joe Bloggs · 2023-03-14 22:00
what the actual fuarrrk is the point of futures / promises / any of this new async crap since 2010? is it just kiddos learning what a blocking function is?
BloodCat · 2023-03-14 22:08
>>34768
it's the kiddos not having to learn what pthreads is
Turner · 2023-04-11 06:50
>>34768
It's just a way to model asynchronous tasks to look like synchronous code. With blocking functions you can write down "Do this, then this, then this, then this." Futures/promises/reactive streams enable this method of programming, with would-be blocking functions able to hand over the work to another thread, or thread scheduler, or poll/epoll/select event loop, or what have you.
Also, Reactive programming specifically has some interest in interactive graphics models a data pipeline, which works well as a component in an alternative form of MVC.
Agent Smith · 2023-09-29 15:42
>>37267
I have some confusion about how you separate out both (2) and (3).
In (2) you mention lightweight tasks / coroutines, but then you list Rust's Futures as an example. I admit I don't use Rust, but looking at its signature, it appears to be more like Reactive programming, which I think could be either equivalent to (3) or a fourth thing altogether.
I also don't know why (2) is considered to have the colored function program vs (3), where goroutines seem to be closer to having multple concurrent Actors perform blocking tasks and communicating the results via channels rather than changing a function signature.
I think that (2) should just be concurrent tasks / the Actor model, with its con being it requires a runtime, and (3) should be both callbacks and reactive programming, since the signature (X, Y, Z) => Future<Foo> is equivalent to (X, Y, Y, (Foo) -> ...) in terms of colored functions.
Mentat · 2023-09-29 16:48
>>37295
I apparently completely misremembered how goroutines worked, so you should disregard any comments about Golang by me.
Rust's Futures are somewhat similar to JS' or Python's async functions. Instead of a function call immediately doing something, it instead constructs and returns an object. You then need to poll this object (a state machine, quite literally) using await if from another "async fn" or with the poll() method.
pub enum Poll<T> {
Ready(T),
Pending,
}
pub trait Future {
type Output;
// Required method
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
This is alright if you are already using a framework like tokio or async_std, but using futures outside of one is annoying since you need to provide a "Context", which is used to provide a "Waker" that can be used by the task to wake the "Executor" which is what actually runs the futures. There's also other annoying stuff with lifetimes and "pinning" due to futures being able to have self-references.
The main difference is that with the model I'm proposing the caller does not need to keep track of any returned objects. Instead, the callee registers itself to an object. If the object relies on e.g. io_uring then you just need to poll it and call a function like you always do. If the object uses regular blocking I/O there is nothing to keep track of at all.
Whereas if you'd use a Future there is always something you need to poll even with blocking I/O. It can get stuck too (you just don't know) so busy looping is a poor idea which means you need to provide a proper waker etc etc. On top of that you can't easily switch between frameworks because they all provide their own I/O objects and traits. Making your async functions generic for that is very, very annoying so most people don't bother and tell you to use tokio, locking many async libraries to a single runtime.
Otto+Mentat · 2023-09-30 08:59
>>37296
It seems that what I'm trying to do turns out to be continuation-passing style but without making a program look ugly as sin.
At least, from what I gather CPS is essentially (using Python)
def cps_add(x, y, f):
return f(x + y)
cps_add(1, 2, print) AKA callbacks. All articles seem to hugely over-complicate it though. Please correct me if I misunderstand.
Davis · 2023-09-30 13:33
>>37296
>>37301
Thank you, I better understand what you're saying.
Judging from the code, I don't see where continuations move down to an event loop, or some other scheduler. Based on the example, this looks synchronous to me. Does this paradigm only replace call/ret with jmp/jmp?
Otto+Mentat · 2023-09-30 14:39
>>37302
Lets say you are reading from a file using io_uring. You have the following structures (pseudo-code):
record IoUring {
# ...
}
record File {
io_uring: SharedReference[IoUring],
fd: FileDescriptor,
} You have an event loop which kickstarts itself by calling some entry function, which then submits some I/O operations, e.g.:
# Still trying to figure out how to make generics work, so for now pretend the compiler figures it out automagically.
fun do_stuff(...) {
# ...
{ data: { file: file, buffer: buffer }, status: var status } = await read(file, buffer);
# ...
}
record ReadData {
file: File,
buffer: Bytes,
}
record IoRequest[T, U] {
data: T,
next_fn: Fun(IoResult[T], U),
next_data: U,
}
record IoResult[T] {
status: IoStatus,
data: T,
}
fun read[U](file: File, buffer: Bytes, offset: u64, next_fn: Fun(IoResult[ReadData], U), next_data: U) {
var ioring = incr_refcount(file.io_uring);
var sqe = io_uring_get_sqe(ptr(ioring));
io_uring_prep_read(sqe, file.fd, ptr(buffer), len(buffer), offset);
var data = ReadData { buffer, file };
var request = IoRequest { data: data, next_fn: next_fn, next_data: next_data };
io_uring_sqe_set_data(request);
io_uring_submit(ptr(ioring));
become main_event_loop(ioring);
} The main "loop" function then looks like this
fun main_event_loop(ioring: SharedReference[IoUring]) {
# Wait for at least 1 event to be completed.
# This returns immediately if an entry is already available.
# Technically this function is blocking... Only happens when there is nothing else to do though.
var cqe = wait_for_one_cqe(ptr(ioring));
var status = get_status(cqe);
{ data: var data, next_fn: var next_fn, next_data: var next_data } = io_uring_cqe_get_data(cqe);
var result = IoResult { status: status, data: data };
become next_fn(result, next_data);
} So
- do_stuff() does a tail call to read() (await is desugared into `become do_stuff_bbXX()`)
- read() prepares a I/O submission, then does a tail call to the main loop function.
- main_event_loop() then checks and optionally waits for a completion entry to become available, then does a tail call to the user-defined function (`do_stuff_bbXX()`).
Note that you can have multiple entries in the queue at any time. You may have a more complicated function that does this:
fun do_stuff_concurrently(...) {
# ...
var file2 = dup(file);
# read_nowait() does not call main_event_loop().
# That's a detail I missed admittedly. This code will only work nicely with async runtimes which isn't ideal,
# though the regular do_stuff() still works with both.
# There's probably a better way to abstract this, but I need think more about it.
read_nowait(file, buffer_a, handler_fn_a); # (1)
read_nowait(file2, buffer_b, handler_fn_b); # (2)
# ...
become main_event_loop(ioring); # (1) and (2) run concurrently at this point.
} or something similar. do_stuff_concurrently() will only work with async runtimes, but do_stuff() works with both.
Now lets say you have some small application that doesn't need a fancy runtime for handling massive amounts of I/O. In that case you can replace the entire body of read() with:
fun read[U](file: File, buffer: Bytes, offset: u64, next_fn: Fun(IoResult[ReadData], U), next_data: U) {
var status = platform_specific_read(file.fd, ptr(buffer), len(buffer), offset);
var data = ReadData { buffer, file };
var result = IoResult { status: status, data: data };
become next_fn(result, next_data);
} So with the same user code you can make use of both
- regular blocking I/O calls with little overhead. It's close to a regular function call.
- a runtime that handles I/O asynchronously
which should be especially useful for library writers, since you don't need to create the same library twice (one for sync, another for async).
There's still a lot of details I need to work out, but this is the gist of it.
Zuse · 2023-10-03 13:30
async is the same dip1sht idiom people in C++ and C# and visual basic were jacking off to in the late 90s and it has nothing of value, its just passing callbacks around and everything is running one one thread. and the biggest joke is that ordering still matters. is reactive programming just as bad?
Unidentified · 2023-12-05 04:23
Is there a way to create reactive chains without the side-effect of getting a gigant callback hell?
I use a transformation function (called map), and my chain aways looks like a hadouken code.
St. IGNUcius · 2024-01-03 07:36
>>37825
Having Promises/Futures in your language with a range of infix operations that transform the promise should do the trick. Most OOP languages define methods to Promises that achieve this, so it can become mostly linear, e.g:
getEntityFromDataStore()
.flatMap(res -> res.active ? Promise.just(res) : Promise.error(new ResourceGoneError(...)))
.map(res -> transformToResponseEntity(res));
Knuth · 2024-08-07 08:51
>>34768
>>34769
I think this misses the point of async/await quite a bit. Threads are a very convenient abstraction when your program can be expressed as several "workers", each assigned its own thing that it's supposed to do. But for some problems, the natural way to express the solution is as a computation that branches and merges and branches and merges again. For example, you may want to do some calculation, download 5 files from 5 different servers, do some calculation again, run an SQL query that returns N results, then run N NoSQL queries that don't depend on one another, one for each result from the SQL query. For such "branchy" computations, starting and joining threads all the time would be both inconvenient and inefficient. On the other hand, async/await can express them in a natural and readable way. Even better than raw async/await is "structured concurrency" - see e.g. https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
>>37267
If it's a high-level language, my vote is definitely for option (2). (1) can be fast but it's also error-prone in large programs, and (3) makes your code suffer from a steady march to the right with the constant nesting.
You said "functional" language, so my mind went immediately to Haskell-style monads, but it looks like your language isn't purely functional. It looks more like a mix of Austral, Rust, and Pony.
Your solution sounds somewhat like continuation-passing style. It works well when you're just doing one think asynchronously, but are you sure it's going to work well when you're doing heavily "branching" computations like this?
var futureA = async doThing(A);
var futureB = async doThing(B);
var futureC = async doThing(C);
await futureA;
await futureB;
await futureC; I recommend you read withoutboats's blog posts about async. He's an important contributor to async Rust and has already grappled with these tradeoffs before.
https://without.boats/blog/why-async-rust/
https://without.boats/blog/let-futures-be-futures/
I would also question whether "function coloring" is actually a problem. A "function" that's been transformed by the compiler into a state machine is a genuinely different thing than a regular function, just executing lines one after the other. I think this should be indicated in the function's signature. In fact, I use Python and I consider it a design flaw that generator functions aren't indicated clearly with some kind of syntax like "gen def", the way that async functions are indicated with "async def". Without such an indication, you have to visually scan the whole function for the "yield" keyword to see whether it's a generator.
On another note, there are several meaningfully different ways to do async that you've grouped together under option (2) and glossed over. You could do fully preemptive concurrency, basically reimplementing threads in user space (like current Go); cooperative concurrency but with implicit yield points (like older Go); or cooperative concurrency with explicit yield points (like Python and Rust). My vote is for implicit if it's a high-level concurrency-focused language, and explicit if it's more of a systems language.
t · 2024-08-10 07:44
>>27681
Actor model, Erlang and BEAM clear all this synchead cope. Everything else is a crutch not worth investing your time.
Nemissa · 2024-08-28 08:27
>>39799
If you're going to go with an entire runtime and preemptive concurrency instead of lightweight cooperative concurrency, I think that the CSP model (somewhat similar to Go channels and Ada rendezvous) is actually better than the actor model. CSP makes it easier to test and analyze the properties of your concurrent system. If something is going to deadlock in production, it'll probably deadlock in testing too. With actors, you have potentially unbounded mailboxes, which can hide issues. Something can work well at low loads but then blow up when the load is high.
space shuttle · 2025-09-19 04:54
bump
>>27681
>Submission queue polling
>One of the goals of io_uring is to provide a means for efficient I/O. To this end, io_uring supports a polling mode that lets you avoid the call to io_uring_enter(2), which you use to inform the kernel that you have queued SQEs on to the SQ. With SQ Polling, io_uring starts a kernel thread that polls the submission queue for any I/O requests you submit by adding SQEs. With SQ Polling enabled, there is no need for you to call io_uring_enter(2), letting you avoid the overhead of system calls. A designated kernel thread dequeues SQEs off the SQ as you add them and dispatches them for asynchronous processing.
I don't understand this. A kernel thread is created to poll (so, to busy loop??). But the purpose of async IO is to not have threads? At least part of the point is, right? Because
>There is no thread https://blog.stephencleary.com/2013/11/there-is-no-thread.html