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