5 ms·
You never have such issues in well-designed programs with a main event loop where all possible events are waited on. Your program is either idle, or waken up by
by bluetomcat 6y ago
You never have such issues in well-designed programs with a main event loop where all possible events are waited on. Your program is either idle, or waken up by a set of events (any kind of IO, a timeout, or a signal from another thread). You handle all events by dispatching them in a timely manner to the responsible code, and wait again. Of course you need non-blocking APIs or dedicated threads for the IO and compute-intensive stuff.
The whole async/await thing is just a convenience feature that looks great but distracts you from thinking about the true concurrency of your program.
- zozbot234 6y agoAsync await notation is for write "dispatch" based program you describe, as straight line code. Is convenience feature, but like syntactic sugar only.
- chriswarbo 6y ago> well-designed programs with a main event loop This is a massive over-generalisation. In particular, you're making a huge assumption that looping is possible, leaping from there to having loops be desirable, then pole-vaulting to claim that anything not architected entirely around looping is "not well-designed". Based on this generalisation, I'm going to guess your experience is limited to languages which (a) have some sort of looping construct built-in (e.g. a `for` or `while` keyword), (b) do not eliminate tail-calls and (c) either don't support, or discourage (perhaps until recently) higher-order programming. This would include languages like C/C++/Go, Python/PHP/Perl, Java/C#, etc. In contrast, I consider looping to be a problematic, error-prone construct in general (for reasons outlined at http://lambda-the-ultimate.org/node/4683#comment-74302 http://lambda-the-ultimate.org/node/4683#comment-74302 ). Recursion is much safer, in languages which eliminate tail-calls. I also consider "main loops" to be an anti-pattern (for reasons outlined at http://disq.us/p/kk1da6 http://disq.us/p/kk1da6 ). They break all sorts of modularity/encapsulation; force bizzare control flow patterns (e.g. lots of highly non-local 'returns', AKA unlabelled GOTOs); force us to make arbitrary (yet highly impactful) decisions between 'carrying on' or 'trampolining off main' (these decisions are often hard to change after-the-fact, regardless of their suitability); require lots of boilerplate to marshal and manage intermediate values which, by their nature, must conflate all sorts temporary values from the entire codebase into amalgamated blobs; and so on. Rather than shoehorning important logic and decisions into a global, manually-kludged tail-call trampoline (AKA a 'main loop'); I find it far easier to write, reason-about and debug simple code which performs a 'step' then tail-calls to the next. As far as concurrency goes, I would claim that "well-designed programs" with control following data flow, it's usually pretty trivial to spin off parts as Futures/etc. as needed. A type system will tell us where to place the necessary calls to "map", etc. as needed.
- logicchains 6y ago>In contrast, I consider looping to be a problematic, error-prone construct in general (for reasons outlined at http://lambda-the-ultimate.org/node/4683#comment-74302 http://lambda-the-ultimate.org/node/4683#comment-74302 ). Recursion is much safer, in languages which eliminate tail-calls. A "main loop" doesn't necessarily imply an actual loop. A game written in Haskell or Scheme probably has a main loop implemented via recursion, but it's still called a main loop. "Main tail-calling function" sounds somewhat ugly. >Far better to write functions which perform a 'step' then tail-call the next function. This sounds like a recipe for spaghetti. The "main loop" approach is much more conducive to the clean approach of "functional core, imperative shell" (https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell https://www.destroyallsoftware.com/screencasts/catalog/funct...), where IO, asynchrony and the like are cleanly separated out from business logic.
- chriswarbo 6y ago> A game written in Haskell or Scheme probably has a main loop implemented via recursion, but it's still called a main loop If such a 'main loop' is a single function which (a) dispatches to one of a bunch of 'handlers' then (b) calls itself recursively, I would still call it a 'main-loop anti-pattern'. More formally, it's likely to be in "defunctionalised" form https://blog.sigplan.org/2019/12/30/defunctionalization-everybody-does-it-nobody-talks-about-it https://blog.sigplan.org/2019/12/30/defunctionalization-ever... The problem with such 'main loops', whether actual loops or self-recursive functions, is that control flow has to keep doubling-back to this 'central dispatcher'. In the case of actual 'main loops' this is often an unavoidable consequence of lacking tail-call elimination. In languages with tail-call elimination, we're just over-complicating the problem. > This sounds like a recipe for spaghetti. The control flow for a 'main loop' program goes like this: main function fooHandler barHandler | +------call-------+ | +-----return------+ | +------call---------------------+ | +-----return--------------------+ | ... If we have tail-call elimination, the handlers can just call the 'main loop' instead of returning (this is just a CPS transformation), e.g.: main function fooHandler main function barHandler main function | +----call--------+ | +-----call----------+ | +------call----+ | +----call----+ | ... Even this simple transformation looks less like spaghetti to me. Calls to this 'main function' now have much more context, compared to "top level" calls to itself. Hence we can avoid special-cases and handler-specific checks migrating their way into the 'main function'; even if we want such things, we don't need to pollute the 'main function' itself (or defunctionalised intermediate data) with such cruft; we could just pass such things as extra arguments, from those call-sites which need them. Tugging at this thread, we'll often find that most of the 'main function' could be moved to its callers, and any remaining functionality may be better in a generic 'plumbing' operation like a monadic bind (the latter being a "programmable semicolon" after all ;) ). > The "main loop" approach is much more conducive to the clean approach of "functional core, imperative shell" I don't see why. The "functional core, imperative shell" approach is mostly about writing as much as possible using pure functions, then 'lifting' them to the required effectful context (via map, join/>>=, etc.); in contrast to the common approach of writing everything within the required context to begin with (e.g. making everything "IO" just because we need one 'printLn' somewhere). That doesn't require manually writing a global "main loop". For example I like to use polysemy, where all the business logic can remain pure, and the `main` function just declares how to 'lift' the application to `IO`. Under the hood it uses a free monad, which dispatches to 'handlers'; but (a) that's all automatically managed, and (b) it recurses through calls spread across the application, rather than returning to a self-calling "main function".