[HN Gopher] Blocking code is a leaky abstraction
       ___________________________________________________________________
        
       Blocking code is a leaky abstraction
        
       Author : zdw
       Score  : 77 points
       Date   : 2024-10-19 20:24 UTC (1 days ago)
        
 (HTM) web link (notgull.net)
 (TXT) w3m dump (notgull.net)
        
       | paulyy_y wrote:
       | Or just use fibers and avoid icky futures.
        
         | aphantastic wrote:
         | Doesn't change the core issue, which is that if you want to do
         | multiple operations simultaneously that take time to execute,
         | you're going to need to write _stuff_ to handle that which you
         | wouldn't otherwise. Whether it's channel piping or async
         | awaiting is semantics.
        
           | PaulDavisThe1st wrote:
           | It's not just semantics. With synchronous code, the blocking
           | aspects are invisible in when reading code:
           | size_t rcnt = read (fd, buf, sizeof (buf)); /* nobody knows
           | if we will block */
           | 
           | With some async code, the fact that there will be a wait and
           | then a continuation is visible from just reading the code.
        
       | nemetroid wrote:
       | The author seems to reinforce the original point, that the async
       | paradigm ends up working best in an all-or-nothing deal. Whether
       | the difficulties in interfacing the paradigms should be
       | attributed to the blocking part or the async part does not really
       | matter for the practical result: if calling blocking code from
       | async code is awkward and your project's main design is async,
       | you're going to end up wanting to rewrite blocking code as async
       | code.
        
       | PaulDavisThe1st wrote:
       | Sometimes, things need to leak.
       | 
       | For example, the fact that code actually takes time to execute
       | ... this is an abstraction that should almost certainly leak.
       | 
       | The fact that some data you want is not currently available ...
       | whether you want to obscure that a wait is required or not is up
       | for debate and may be context dependent.
       | 
       | "I want to always behave as though I never have to block" is a
       | perfectly fine to thing to say.
       | 
       | "Nobody should ever have to care about needing to block" is not.
        
         | fargle wrote:
         | > this is an abstraction that should almost certainly leak.
         | 
         | that's not a leak, that _is the abstraction_ for procedural
         | /imperative code, or at least an important part of it.
         | essentially: "each step occurs in order and the next step
         | doesn't happen until the former step is finished"
        
           | PaulDavisThe1st wrote:
           | That's not a particularly interesting description for
           | multithreaded code. The procedural/imperative element of it
           | may be true of any given thread's execution, but with the
           | right user-space or kernel scheduler, you may or may not care
           | what the precise sequencing of things is across threads. This
           | is true whether you're using lightweight user-space threads
           | ("fibers") or kernel threads.
        
             | fargle wrote:
             | of course, but in that case it's not the base imperative
             | abstraction that's leaky - it's the multi-thread/multi-core
             | abstraction built on top of it - it's _almost_ as easy as
             | multiple threads happening in parallel. until it isn 't.
             | 
             | the multi-thread abstraction attempts to be: "hey, you know
             | how that imperative model works? now we can have more than
             | one of these models (we'll call them threads!) running in
             | parallel. neat, huh?"
             | 
             | it's things like memory barriers and race conditions, etc.
             | that are the "leaks". but it's the threading/multi-core
             | abstractions that are leaky not (back to the article) the
             | "blocking" code.
             | 
             | i can write single-thread "blocking" code all day long that
             | does all kinds of interesting non-trivial things and
             | _never_ have to worry about leaks wrt. blocking, in-order
             | execution, etc. even in the presence of out-of-order
             | execution processors and optimizing compilers, VMs, multi-
             | user OS 'es, etc. effects are observably identical to the
             | naive model.
             | 
             | the author didn't do a good job of clearly defining
             | anything, but i bristle at the idea that the basic
             | "blocking" code abstraction whats leaky - it's the async,
             | threads, event-driven models, etc. that are (necessarily) a
             | bit leaky when they break the basic imperative blocking
             | model or require accommodation of it.
        
               | PaulDavisThe1st wrote:
               | good points. i'd add, though, that priority inversion is
               | specifically a leakage of the blocking nature of some
               | parts of the imperative code into the thread model.
               | similarly, the implications of locks for real time code
               | (something i work on) is another example of blocking
               | leaking into a thread model.
        
       | klodolph wrote:
       | What is the difference between code that blocks waiting for I/O
       | and code that performs a lengthy computation? To the runtime or
       | scheduler, these are very different. But to the caller, maybe it
       | does not matter _why_ the code takes a long time to return, only
       | that it does.
       | 
       | Async only solves one of these two cases.
       | 
       | I'd like to draw an analogy here to [?] "bottom" in Haskell. It's
       | used to represent a computation that does not return a value. Why
       | doesn't it return a value? Maybe because it throws an exception
       | (and bubbles up the stack), or maybe because it's in an infinite
       | loop, or maybe it's just in a _very long_ computation that
       | doesn't terminate by the time the user gets frustrated and
       | interrupts the program. From a certain perspective, sometimes you
       | don't care _why_ [?] doesn't return, you just care that it
       | doesn't return.
       | 
       | Same is often true for blocking calls. You often don't care
       | whether a call is slow because of I/O or whether it is slow
       | because of a long-running computation. Often, you just care
       | _whether_ it is slow or _how slow_ it is.
       | 
       | (And obviously, sometimes you do care about the difference. I
       | just think that the "blocking code is a leaky abstraction" is
       | irreparably faulty, as an argument.)
        
         | rileymat2 wrote:
         | Async is worse than leaky it often goes viral because many
         | other parts need to be async to call it.
        
           | gwbas1c wrote:
           | It's better to think of "async" as indicating that a code
           | will do something that blocks, and we're allowing our process
           | to manage its blocking (via Futures) instead of the operating
           | system (via a context switch mid-thread.)
           | 
           | I would argue a few things:
           | 
           | First: You need to be aware, in your program, of when you
           | need to get data outside of your process. This is,
           | fundamentally, a blocking operation. If your minor refactor /
           | bugfix means that you need to add "async" a long way up the
           | stack, does this mean that you goofed on assuming that some
           | kind of routine could work only with data in RAM?
           | 
           | Instead: A non-async function should be something that you
           | are confident will only work with the data that you have in
           | RAM, or only perform CPU-bound operations. Any time you're
           | writing a function that could get data from out of process,
           | make it async.
        
             | hackit2 wrote:
             | You could make the proposition that sequential code is
             | inherently asynchronous in modern operating systems,
             | because the kernel inherently abstracts the handling of
             | blocking/unblocking your process.
        
               | smw wrote:
               | Sure, but doesn't that remove the usefulness of the
               | words?
        
             | rileymat2 wrote:
             | In many application use cases, that is an implementation
             | detail that should not be a concern of higher levels. The
             | ramifications may not even be known by the people at higher
             | levels.
             | 
             | Take something very common like cryptographic hashing, if
             | you use something like node.js you really don't want to
             | block the main thread calculating an advanced bcrypt hash.
             | It also meets all of your requirements that data not come
             | from outside ram and is very CPU bound.
             | 
             | Obviously, if you are directly calling this hashing
             | algorithm you should know, however, the introduction of a
             | need to hash is completely unpredictable.
        
         | akoboldfrying wrote:
         | >What is the difference between code that blocks waiting for
         | I/O and code that performs a lengthy computation?
         | 
         | To the caller _of that specific function_ , nothing. To the
         | entire program, the difference is that other, useful CPU work
         | can be done in the meantime.
         | 
         | This might not matter at all, or it might be the difference
         | between usable and impractically slow software.
        
           | klodolph wrote:
           | Yes, you understand exactly the point I'm making.
        
           | Joker_vD wrote:
           | > To the caller _of that specific function_ , nothing.
           | 
           | And that's what makes async code, not the blocking code, a
           | leaky abstraction. Because abstraction, after all, is about
           | distracting oneself from the irrelevant details.
        
             | akoboldfrying wrote:
             | That isn't my understanding of a leaky abstraction. An
             | abstraction leaks when it's supposed to always behave in
             | some certain, predictable way, but in practice, sometimes
             | it doesn't. When does an async function not behave the way
             | it's supposed to?
        
               | ljm wrote:
               | My understanding of a leaky abstraction is that the
               | abstraction itself leaks out details of its design or
               | underlying implementation and requires you to understand
               | them. What you seem to describe is a bug, edge case, or
               | maybe undefined behaviour?
               | 
               | For example, an ORM is a leaky abstraction over an RDBMS
               | and SQL because you inevitably have to know details about
               | your RDBMS and specific SQL dialect to work around
               | shortcomings in the ORM, and also to understand how a
               | query might perform (e.g will it be a join or an N+1?).
        
               | 8note wrote:
               | I don't really think that the async or blockingness is
               | the leak, but that the time taken to process is not
               | defined in either case, and you can leak failure criteria
               | either way by not holding to that same time.
               | 
               | People can build to your async process finishing in 10ms,
               | but if suddenly it takes 1s, it fails
        
       | worik wrote:
       | Asynchronous code is the bees knees
       | 
       | Async/await is a horrible fit for Rust.
       | 
       | > Some of this criticism is valid. async code is a little hard to
       | wrap your head around, but that's true with many other concepts
       | in Rust, like the borrow checker and the weekly blood sacrifices.
       | Many popular async libraries are explicitly tied to heavyweight
       | crates like tokio and futures, which aren't good picks for many
       | types of programs. There are also a lot of language features that
       | need to be released for async to be used without an annoying
       | amount of Boxing dynamic objects.
       | 
       | Yes. So don't do it
       | 
       | It is entirely possible to write asynchronous code without using
       | asyc/await.
       | 
       | async/await is a fun paradigm for garbage collected systems. It
       | is a horrible mess in Rust
        
         | 0x1ceb00da wrote:
         | It's the only real solution if you need tens of thousands of
         | concurrent tasks. Real threads use too much memory.
        
           | smw wrote:
           | Or you use something like goroutines or Elixir processes
           | where green threads are preemptively scheduled.
        
       | Spivak wrote:
       | It seems like the magic for the author is in the annotation.
       | There's still two kinds of functions "fearlessly run in the UI
       | thread" and "must be run in a worker" and async is great in that
       | it greatly expands the number of functions in that first pool but
       | it's effectively the same thing as the [blocking] annotation but
       | the other way and not totally complete. Because there are async-
       | safe but not async functions.
       | 
       | So to me it seems like it all leaks because in all cases what's
       | desired is some external signal about how the function behaves. A
       | "badly written" async function can stall the loop and that isn't
       | conveyed only by the async tag. It's rare to do this but you can
       | and there might even be times where this makes sense[1]. But it
       | happens that by convention async and ui-thread-safe line up but
       | there's a secret other axis that async is getting mixed up in.
       | 
       | [1] Say running a separate event loop in a background thread used
       | for CPU intensive tasks that occasionally perform IO and you want
       | to make the most use of your thread's time slice.
        
       | wavemode wrote:
       | > Frankly, I don't think async code is leaky at all, and the ways
       | that it does leak are largely due to library problems.
       | 
       | Huh? One could argue the exact same in opposite direction -
       | blocking code isn't leaky at all, just wrap it in a Future. And
       | if doing that is difficult because of !Send data, then those are
       | just "library problems". You could always just use a threadsafe
       | library instead, right?
        
       | rtpg wrote:
       | "asynchronous code does not require the rest of your code to be
       | synchronous" fails the smell test.
       | 
       | Many APIs are shipping async-only. You can't stick calls to those
       | APIs in the middle of your sync code in many cases (unless you
       | have an event loop implementation that is re-entrant). So... you
       | gotta convert your sync code to async code.
       | 
       | I am curious as to what a principled definition of a "blocking"
       | function would be. I suppose it's a call to something that ends
       | up calling `select` or the like. Simply saying "this code needs
       | to wait for something" is not specific enough IMO (sometimes
       | we're just waiting for stuff to move from RAM to registers, is
       | that blocking?), but I'm sure someone has a clear difference.
       | 
       | If you care about this problem enough, then you probably want to
       | start advocating for effect system-like annotations for Rust.
       | Given how Rust has great tooling already in the language for
       | ownership, it feels like a bit of tweaking could get you effects
       | (if you could survive the eternal RFC process)
        
         | PaulDavisThe1st wrote:
         | > I am curious as to what a principled definition of a
         | "blocking" function would be.
         | 
         | It's one where the OS puts your (kernel) thread/task to sleep
         | and then (probably) context switches to another thread/task
         | (possibly of another process), before eventually resuming
         | execution of yours after some condition has been satisfied
         | (could be I/O of some time, could be a deliberate wait, could
         | be several other things).
        
           | akoboldfrying wrote:
           | That seems a necessary but not sufficient condition, since a
           | pre-emptively multitasking OS may do this after almost any
           | instruction.
           | 
           | Not only that, but any OS with virtual memory will almost
           | certainly context-switch on a hard page fault (and perhaps
           | even on a soft page fault, I don't know). So it would seem
           | that _teading memory_ is sufficient to be  "blocking", by
           | your criterion.
        
             | PaulDavisThe1st wrote:
             | 1) I deliberately left it open to including page faults by
             | design. If you do not understand that reading memory
             | _might_ lead to your process blocking, you have much to
             | learn as a programmer. However, I would concede that that
             | this is not really part of the normal meaning of
             | "blocking", despite the result being very similar.
             | 
             | 2) I did not intend to include preemption. I should reword
             | it to make it clear that the "condition" must be something
             | of the process' own making (e.g. reading from a file or a
             | socket) rather than simply an arbitrary decision by the
             | kernel.
        
           | rtpg wrote:
           | This feels mostly right to me. I think that you get into
           | interesting things in the margins (is a memory read blocking?
           | No, except when it is because it's reading from a memory-
           | mapped file!) that make this definition not 100% production
           | ready.
           | 
           | But ultimately if everyone in the stack agrees enough on a
           | definition of blocking, then you can apply annotations and
           | have those propagate.
        
             | kevincox wrote:
             | > except when it is because it's reading from a memory-
             | mapped file
             | 
             | Where "memory mapped file" includes your program
             | executable. Or any memory if you have swap space available.
             | 
             | And any operations can be "blocking" if your thread is
             | preempted which can happen at basically any point.
             | 
             | So yes, everything is blocking. It is just shades of grey.
        
               | rtpg wrote:
               | But this isn't hemming to the definition brought up by
               | GP. "I will now, of my own accord, sleep so the OS
               | scheduler takes over" is fairly precise. And it's
               | different from both just doing an operation that takes
               | time... and different from the program doing an operation
               | that, incidentally, the OS sees and then forces a sleep
               | due to some OS-internal abstraction
               | 
               | But you think about this too much and you can easily get
               | to "well now none of my code is blocking", because of how
               | stuff is implemented. Or, more precisely, "exactly one
               | function blocks, and that function is select()" or
               | something.
        
           | o11c wrote:
           | For reference, there are 2 lists of such functions in
           | signal(7).
           | 
           | The first list is for syscalls that obey `SA_RESTART` -
           | generally, these are operations on single objects - read,
           | lock, etc.
           | 
           | The second list is for syscalls that don't restart -
           | generally, these look for an event on any of a set of objects
           | - pause, select, etc.
        
           | leni536 wrote:
           | OS threads can be put to sleep for many reasons, or they can
           | be preempted for no explicit reason at all.
           | 
           | On such reason could be accessing memory that is currently
           | not paged in. This could be memory in the rext section,
           | memory mapped from the executable you are running.
           | 
           | I doubt that you would want to include such context switches
           | in your "blocking" definition, as it makes every function
           | blocking, rendering the taxonomy useless.
        
         | vlovich123 wrote:
         | On the other hand, it might flag a code smell to you in that
         | you're injecting I/O into a code path that had no I/O before
         | which forces you to either make annotations making that
         | explicit for the future or to realize that that I/O call could
         | be problematic. There's something nice about knowing whether or
         | not functions you're invoking have I/O as a side effect.
        
       | plorkyeran wrote:
       | The author has a very strange understanding of the idea of a
       | "leaky abstraction". AppKit requiring you to call methods on the
       | main thread is just not an abstraction at all, leaky or
       | otherwise.
        
       | samatman wrote:
       | There's a conflation going on here which is a blocker, shall we
       | say, for understanding the two sides of this coin. The antonym of
       | async is not blocking, it is synchronous. The antonym of blocking
       | I/O is evented I/O.
       | 
       | Stackless coroutines a la Rust's async are a perfectly reasonable
       | control flow primitive to use in 'blocking' contexts, which
       | properly aren't blocking at all since they aren't doing I/O, I'm
       | thinking of generator patterns here.
       | 
       | It's also entirely possible, and not even difficult, to use
       | evented I/O in synchronous function-calls-function style code.
       | You poll. That's the whole deal, somewhere in the tick, you check
       | for new data, if it's there, you eat it. This does not require
       | async.
       | 
       | I found the "what if the real abstraction leak was the friends we
       | made along the way" portion of the argument. Unconvincing. When
       | people complain about async leaking, what they mean is that they
       | would like to be able to use libraries without have to start a
       | dummy tokio in the background. That's what's getting peas in the
       | mashed potatoes, that's what annoys people.
       | 
       | "Abstraction leak" is a sort of idiomatic way to complain about
       | this. Focusing on the slogan does nothing to address the
       | substance, and "have you considered... that, a function, might
       | take a while, and perhaps _that_ , _that_ is an  'abstraction
       | leak'? eh, eh?" is. Well it probably seemed clever when it was
       | being drafted.
        
       | skybrian wrote:
       | This is the opposite in JavaScript (and similar single-threaded
       | languages) where sync code is atomic and normally can't do I/O.
        
         | akira2501 wrote:
         | node.js has a whole plethora of synchronous IO methods. they're
         | actually really nice. or have I misunderstood your meaning?
        
           | maronato wrote:
           | Node uses libuv[1] for all its IO operations[2]. The sync
           | versions still use libuv, but block the main thread until
           | they receive the result.
           | 
           | [1] https://libuv.org [2]
           | https://nodejs.org/en/learn/asynchronous-work/overview-of-
           | bl...
        
         | throwitaway1123 wrote:
         | You can perform file I/O in JavaScript synchronously. The
         | localStorage API in the browser is synchronous (and in Node via
         | the --experimental-webstorage option), and of course requiring
         | a CommonJS module is also synchronous (and there are many other
         | sync filesystem APIs in Node as a sibling comment pointed out).
         | 
         | You just can't perform _network_ I /O synchronously. Although a
         | network attached file system allows for both network and file
         | I/O technically, but that's a really pedantic point.
        
           | Izkata wrote:
           | > You just can't perform _network_ I /O synchronously.
           | 
           | Sure you can, you just shouldn't ever do it because it blocks
           | the UI: https://developer.mozilla.org/en-
           | US/docs/Web/API/XMLHttpRequ...
        
             | throwitaway1123 wrote:
             | Yeah I should've said there's no Node API for making
             | synchronous HTTP requests (unless you count executing a
             | child process synchronously). Even the older http.request
             | API used in Node prior to the introduction of fetch is
             | async and accepted a callback. Browsers have all sorts of
             | deprecated foot guns though (like the synchronous mode of
             | XMLHttpRequest).
        
       | gwbas1c wrote:
       | > Dependency Dog: If you want to see a good example of a leaky
       | abstraction, consider AppKit. Not only is AppKit thread-unsafe to
       | the point where many functions can only safely be called on the
       | main() thread, it forces you into Apple's terrifying Objective-C
       | model. Basically any program that wants to have working GUI on
       | macOS now needs to interface in Apple's way, with basically no
       | alternatives.
       | 
       | Uhm, every OS that I've developed a GUI for has a similar
       | limitation. The "only call this thing from this thread" is a
       | well-known thread safety pattern. It might not be the pattern
       | that this author prefers, but it is an industry standard pattern
       | for UI.
        
         | koito17 wrote:
         | I agree with your comment. Every UI framework I have used
         | (AppKit, Swing, JavaFX, etc.) is not thread-safe. It requires
         | all UI code in the main thread or, in the case of JavaFX, a
         | dedicated UI thread (not necessarily the main thread, but it
         | must be a single thread where all UI operations occur).
        
           | etcd wrote:
           | WPF was the same IIRC
        
             | solarkraft wrote:
             | yup!
        
           | mike_hearn wrote:
           | Technically in JavaFX you can work with UI objects off the
           | main thread, for instance to load or build up UI in parallel.
           | The single thread requirement only kicks in when UI is
           | attached to a live window (stage).
        
       | ianhooman wrote:
       | Godel covers how it's leaky abstraction all the way down. If you
       | consider a specific chunk of useful async code it still doesn't
       | compose enough of system to do much of use; it can't answer many
       | questions alone or any about itself. We never just have Map() but
       | other functions. Its constant composition of symbolic logic we
       | vaguely agree means something relative to an electrical system of
       | specific properties.
       | 
       | A useful system is "blocked" on continuous development of that
       | system.
       | 
       | These posts are too specific to code, miss the forest for a tree.
       | A more generalized pedantry (like electrical state of systems
       | rather than dozens of syntax sugars to simulate the same old
       | Turing machine) and more frequent spot checks for stability and
       | correctness relative to the ground truth would be great.
       | 
       | Way too much circumlocution around code. Map is not the terrain.
       | I for one am having blast training a state generator that acts
       | like a REPL and (for example) infers how to generate Firefox from
       | a model of its code (among other things).
       | 
       | Excited about the potential to peel away layers of abstraction by
       | generalizing the code with automated assistants.
        
       | akira2501 wrote:
       | I can't buy this premise. The leaky abstraction is that your
       | selected libraries are attempting to be as efficient and as fast
       | as possible while also trying to be fully general. As a result
       | you get the full abstraction layer necessary to do this, which,
       | as you've noted, for any one project is too much.
       | 
       | The complication you seem to be experiencing is other projects
       | have come to rely on these libraries and their particular
       | abstractions and so you've actually got a dependency management
       | problem due to the design of the "core" libraries here or due to
       | a lack of language features to /constrain/ all of them. A
       | critical point of abstraction totally lacking in this ecosystem.
       | 
       | In any case if you look at any language that has contexts that
       | can be externally canceled you should realize this actual
       | solution must be implemented at a higher level than these "async
       | primitives."
        
       | accelbred wrote:
       | I have the opposite experience, working in embedded (C, not
       | Rust...). Building a synchronous API on top of an async one is
       | hell, and making a blocking API asynchronous is easy.
       | 
       | If you want blocking code to run asynchronously, just run it on
       | another task. I can write an api that queues up the action for
       | the other thread to take, and some functions to check current
       | state. Its easy.
       | 
       | To build a blocking API on top of an async one, I now need a lot
       | of cross thread synchronization. For example, nimBLE provides an
       | async bluetooth interface, but I needed a sync one. I ended up
       | having my API calls block waiting for a series of FreeRTOS task
       | notifications from the code executing asynchronously in nimBLE's
       | bluetooth task. This was a mess of thousands of lines of BLE
       | handling code that involved messaging between the threads. Each
       | error condition needed to be manually verified that it sends an
       | error notification. If a later step does not execute, either
       | through library bug or us missing an error condition, then we are
       | deadlocked. If the main thread continues because we expect no
       | more async work but one of the async functions are called, we
       | will be accessing invalid memory, causing who knows what to
       | happen, and maybe corrupting the other task's stack. If any
       | notification sending point is missed in the code, we deadlock.
        
         | hackit2 wrote:
         | The trick with synchronization/parallelism systems is to only
         | communicate over a known yield point this is normally done via
         | queues. It is the only way you get deterministic behavior from
         | your sub-systems or multi-threaded environments.
        
         | jcranmer wrote:
         | Making an asynchronous task into a synchronous task is easy in
         | exactly one scenario: when there is no pre-existing event loop
         | you need to integrate with, so the actual thing the synchronous
         | task needs to do is create the event loop, spin it until it's
         | empty, and then continue on its merry way. Fall off this happy
         | path, and everything is, as you say, utterly painful.
         | 
         | In the opposite direction, it's... always easy (if not entirely
         | trivial). Spin a new thread for the synchronous task, and when
         | it's done, post the task into the event loop. As long as the
         | event loop is capable of handling off-thread task posting, it's
         | easy. The complaint in the article that oh no, the task you
         | offload has to be Send is... confusing to me, since in
         | practice, you need that to be true of asynchronous tasks
         | anyways on most of the common async runtimes.
        
         | dsab wrote:
         | I have the same experience, I like splitting my embedded C
         | microcontroller peripheral drivers into 3 layers:
         | 
         | - header files with registers addresses and bitmasks
         | 
         | - asynchronous layer that starts transactions or checks
         | transaction state or register interrupt handler called when
         | transaction changes states
         | 
         | - top, RTOS primitives powered, blocking layer which
         | encapsulates synchronization problems and for example for UART
         | offers super handy API like this:
         | 
         | status uart_init(int id, int baudrate)
         | 
         | status uart_write(int id, uint8_t* data, int data_len, int
         | timeout_ms)
         | 
         | status uart_read(int id, uint8_t* buf, int buf_len, int
         | timeout_ms, int timeout_char_ms)
         | 
         | Top, blocking API usually covers 95% use cases where business
         | logic code just want to send and receive something and not
         | reinvent the synchronization hell
        
         | wyager wrote:
         | > If you want blocking code to run asynchronously, just run it
         | on another task
         | 
         | What kind of embedded work are you doing exactly? Linux "soft"
         | embedded or MMUless embedded? I don't have infinite NVIC
         | priority levels to work with here... I can't just spin up
         | another preemptively scheduled (blocking) task without eating a
         | spare interrupt and priority level.
         | 
         | Otoh, I can have as many cooperatively scheduled (async) tasks
         | as I want.
         | 
         | Also, at least in Rust, it's trivial to convert nonblocking to
         | blocking. You can use a library like pollster or embassy-
         | futures.
        
         | jerf wrote:
         | "If you want blocking code to run asynchronously, just run it
         | on another task."
         | 
         | This highlights one of the main disconnects between "async
         | advocates" and "sync advocates", which is, when we say
         | something is blocking, what, _exactly_ , is it blocking?
         | 
         | If you think in async terms, it is blocking your entire event
         | loop for some executor, which stands a reasonable chance of
         | being your only executor, which is horrible, so yes, the
         | blocking code is the imposition. Creating new execution
         | contexts is hard and expensive, so you need to preserve the
         | ones you have.
         | 
         | If you think in sync terms, where you have some easy and cheap
         | ability to spawn some sort of "execution context", be it a
         | Haskell spark, an Erlang or Go cheap thread, or even are just
         | in a context where a full OS thread doesn't particularly bother
         | you (a situation more people are in than realize it), then the
         | fact that some code is blocking is not necessarily a big deal.
         | The thing it is blocking is cheap, I can readily get more, and
         | so I'm not so worried about it.
         | 
         | This creates a barrier in communication where the two groups
         | don't quite mean the same thing by "sync" and "async".
         | 
         | I'm unapologetically on Team Sync because I have been
         | programming in contexts where a new Erlang process or goroutine
         | is very cheap for many years now, and I strongly prefer its
         | stronger integration with structured programming. I get all the
         | guarantees about what has been executed since when that
         | structured programming brings, and I can read them right out of
         | the source code.
        
           | winwang wrote:
           | I like your take, and I propose there is another kind of
           | division: function "coloring". An async "effect" type (e.g.
           | futures, or even just 'async function') signals to
           | programmers that there is some concurrency stuff going on
           | around the place(s) you use it and that we need to talk about
           | how we're going to handle it. i.e. rather than a performance
           | issue, it's a correctness/safety/semantics issue.
        
           | jtrueb wrote:
           | "This highlights one of the main disconnects between "async
           | advocates" and "sync advocates", which is, when we say
           | something is blocking, what, exactly, is it blocking?"
           | 
           | When I have work on a cooperatively scheduled executor for
           | optimal timing characteristics. Sending work/creating a task
           | on a preemptive executor is expensive. Furthermore, if that
           | blocking work includes some device drivers with interactions
           | with hardware peripherals, I can't reasonable place that work
           | on a new executor without invalidating hardware timing
           | requirements.
           | 
           | Threads and executors can be infeasible or impossible to
           | spawn. I have 1 preemptive priority and the rest are
           | cooperative on bare metal. I can eat the suboptimal
           | scheduling overhead with a blocking API/RTOS or I need the
           | async version of things.
        
       | anacrolix wrote:
       | This is some bullshit. And it doesn't help that everyone is using
       | async to mean different things.
       | 
       | Blocking/synchronous system calls are not friendly to userspace
       | async runtimes. Explicit async runtimes are a pain in the ass.
       | The best solution imo is a blocking userspace interface that maps
       | into the system however it likes. Go and Haskell do this. Erlang
       | and probably all BEAM languages do too. Nim and Zig let you
       | choose.
       | 
       | Runtimes like Go and Haskell can map onto code that makes
       | blocking system calls for compatibility reasons when needed,
       | through explicit request to the runtime, or detecting that a
       | green thread has blocked.
        
         | smw wrote:
         | This is the right answer! Preemptively scheduled n:m green
         | threads >>>> any async design.
        
       | Joker_vD wrote:
       | > Blocking code is a leaky abstraction
       | 
       | > Asynchronous code does not require the rest of your code to be
       | asynchronous. I can't say the same for blocking code.
       | 
       | Well, you can also say that "file systems are a leaky
       | abstractions. Sector-addressing disk access does not require the
       | rest of your code to access disk sectors directly. I can't say
       | the same for the code that uses files".
       | 
       | First of all, asynchronous code does require the rest of your
       | code to be asynchronous: because, as you've yourself said,
       | blocking code doesn't mesh well with the asynchronous. Second,
       | when your are trying to work with two different levels of
       | abstractions at the same time, of course you will find the
       | presence of the higher-level abstraction (the blocking code, the
       | file system, etc) to be inconvenient: the higher-level
       | abstraction has to make some opinionated choices, or it would not
       | be an abstraction but merely an interface.
       | 
       | So yeah, I guess blocking code is a leaky abstraction, just like
       | file systems, TCP streams, garbage-collected memory, and
       | sequential processor instruction execution. It's still a higher-
       | level abstraction though.
        
       | nurettin wrote:
       | block_on and spawn_blocking are tokio's async abstractions, there
       | is no "blocking code's abstraction" that can leak.
       | 
       | What the article intentionally omits is that I can defer io by
       | getting a handler and polling it in a select loop. And right
       | after that, I can also poll my mpsc queue if I want to. This is
       | how "blocking code" has worked for the last few decades.
        
       | fargle wrote:
       | the author is misusing the "leaky abstraction" idea. in the
       | section "what's in a leak" a rather muddy argument is made that a
       | leak is that which forces you to bend your program to accommodate
       | it. so `leak => accommodation`.
       | 
       | then it immediately conflates the need for that accommodation
       | (difficulty of calling one type of code with another) with
       | "leakiness". so essentially "calling blocking => more
       | accommodation" (e.g. from event loops that shouldn't block).
       | 
       | - that's logically incorrect, even in the author's own argument
       | (A => B, B, therefore A is the affirming the consequent fallacy).
       | 
       | - that's not what a "leaky abstraction" is. not everything that
       | doesn't fit perfectly or is hard is due a "leaky abstraction".
       | rather it's when the simplistic model (abstraction) doesn't
       | always hold in visible ways.
       | 
       | a "leaky abstraction" to "blocking" code might be if code didn't
       | actually block or execute in-order in all situations. until you
       | get to things like spinlocks and memory barriers this doesn't
       | happen. but that's more a leak caused by naively extending the
       | abstraction to multi-core/multi-thread use, not the abstraction
       | of blocking code itself.
       | 
       | i love the passion for `async` from this fellow, but i think he's
       | reacting to "async is a leaky abstraction" as if it were a slur
       | or insult, while misunderstanding what it means. then replies
       | with a "oh yeah, well _your_ construct is twice as leaky "
       | retort.
        
       | amelius wrote:
       | They mean a viral abstraction.
        
       | netbsdusers wrote:
       | I am surprised by some of the comments claiming to find it very
       | hard to make an async API out of a sync one.
       | 
       | I write a hobby kernel with a profoundly asynchronous I/O system.
       | Since it's nice to have synchronous I/O in some places (reading
       | in data in response to a page fault, for example, or providing
       | the POSIX read/write functions), I have to turn it into
       | synchronous I/O in some cases.
       | 
       | Asynchronous I/O notifies its completion in a configurable-
       | per-I/O-packet (called "IOP") way. The default is to signal an
       | event object. You can wait on an event object and when it's
       | signalled, you're woken up. The result is that synchronously
       | sending an IOP is hardly any more work than:
       | iop_send(iop);       wait_for_event(iop->completion_event);
       | return iop->result;
       | 
       | I would be surprised to hear of any system where you can't do
       | similar. Surely any environment has at least semaphores
       | available, and you can just have the async I/O completion signal
       | that semaphore when it's done?
        
         | rileymat2 wrote:
         | I don't think the problem comes so much from async v. sync, it
         | comes from the implementation language/framework.
         | 
         | If you were to wait_for_event (or its equivalent) in the node
         | main thread you will stall the 1 thread handling requests.
         | 
         | This whole problem is not just async/sync, it is the
         | environment you live in.
         | 
         | What we should be questioning is why high level languages in
         | high level frameworks make the programmer even consider the
         | distinction at all.
         | 
         | If I am handling an http request, the async nature of this is
         | an optimization because threads (or worse, processes, I once
         | easily dos'ed an apache system that created a new process for
         | each request) are too expensive at scale.
         | 
         | It should not concern the programmer whether a thread is
         | launched for each request or we are using cooperative
         | multitasking (async). The programmer/application needs that
         | information from that call before that one piece can move
         | forward.
        
       | hinkley wrote:
       | What I like about async and promises is there's just enough
       | bookkeeping to keep people from violating Keenighan's law by too
       | wide a margin. A little goes a long way, especially when any
       | process could be handling several requests at once and make
       | better use of the dead space between steps.
       | 
       | But a friend recently showed me how coroutines work in Kotlin and
       | from my rather extensive experience helping people debug code
       | that's exceeded their Kernighan threahold, this seems a lot less
       | footgun than nodejs. It's far too easy to inject unhandled
       | promise rejection in Nodejs. I think await should be the default
       | not the exception, and Kotlin is closer to that.
        
         | wruza wrote:
         | How do they work in Kotlin? If it's just:
         | result = co_func()
         | 
         | Then that creates an invisible yield point which messes with
         | your "is this a critical section" sense. Suddenly anything may
         | yield and your naive                 pt.x = getX()       pt.y =
         | getY()
         | 
         | turns into the concurrency hell wrt pt's consistency.
         | 
         |  _I think await should be the default not the exception, and
         | Kotlin is closer to that._
         | 
         | I somewhat agree in that it should be an await/nowait keyword
         | pair required at async calls. Not realistic though even in
         | typescript, considering that Promise{} is just a continuation
         | shell that may be passed around freely and doesn't represent a
         | specific function's asynchronicity.
        
       | jongjong wrote:
       | Async/await has been a game-changer. You can tell that this is
       | the case by how much shorter and more reliable code with
       | async/await is compared to alternatives (e.g. which rely on
       | callbacks).
       | 
       | Callbacks are a leaky abstraction IMO because they force you to
       | write a lot of additional logic to handle results in the correct
       | sequence. Obtaining the result of multiple long-running function
       | calls on the same line is insanely messy with callbacks.
       | 
       | I've found that async/await helps to write code which stays close
       | to the business domain. You can design it with multiple layers
       | which take care of a distinct level of abstraction so you can
       | understand what's happening just by reading the main file top to
       | bottom. You don't have to jump around many files to read the
       | functions to understand what they are doing and how their results
       | are combined.
       | 
       | Async/await makes the concurrency aspect as succinct as possible.
       | 
       | That said, I've seen many people writing code with async/await as
       | though they were writing using callbacks... Some people haven't
       | adapted correctly to it. If your async/await code doesn't look
       | radically different from how you used to write code, then you're
       | probably doing something seriously wrong. Ensure that you
       | familiarize yourself with the async/await utility functions of
       | your language like (in JS) Promise.all, Promise.race... Notice
       | how try-catch works with the await keyword...
        
       ___________________________________________________________________
       (page generated 2024-10-20 23:00 UTC)