Re: SBCL Fibers proposal

Christophe Rhodes via Sbcl-devel <[email protected]> Thu, 12 Mar 2026 21:44:56 +0000
Newsgroups gmane.lisp.steel-bank.devel
Message-ID <[email protected]>
Hi,

I've now had a chance to read the proposal and write some observations,
which are attached.  I hope that this can help provide a basis for
useful conversations.

I know there have been some other comments and suggestions.  I think
maybe what would be useful to help make the conversation more concrete
is if there are any brave potential users on this list who would be
willing to do some ecological consideration of (a revised version of)
this proposal: are there existing applications which stand to benefit
right now?  It would be nice to hear from you if so!

Christophe


Christophe Rhodes via Sbcl-devel <[email protected]>
writes:

> Anthony Green <[email protected]> writes:
>
>> This is a very fair question.   I worked with the LLM to generate the
>> proposal based on the current implementation and I am confident in the
>> exact wording.
>
> Thanks.  It's going to take time for me to review this in detail: I hope
> to get some thoughts written down and sent around the end of next week.

_______________________________________________
Sbcl-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/sbcl-devel
sbcl-fibers-notes.org (text/plain, 28 KB)
#+OPTIONS: toc:nil num:nil

* Executive Summary
  This is my personal review of SBCL Fibers: Lightweight Cooperative
  Threads, version dated March 1, 2026, available at
  https://atgreen.github.io/repl-yell/posts/sbcl-fibers/ ([[https://web.archive.org/web/20260308181851/https://atgreen.github.io/repl-yell/posts/sbcl-fibers/][Internet
  Archive link]]).  Perhaps more than usual it is important to stress
  that I do not claim to speak for or on behalf of my employer, and
  all opinions expressed in this review are my own.

  Although there exists implementation code related to this document,
  I have not looked at that in forming this review.

  On balance I believe that this document describes a reasonable
  design for cooperative user-space threads in SBCL.  I have a number
  of high-level questions about the design that might influence the
  overall direction and shape of an implementation; some substantive
  criticisms of aspects of this specific design as expressed in this
  document; and a list of points of detail of various degrees of
  importance.

  I also include a small section reflecting on the process of reading
  the original document and writing this review.
* High level questions
** Structured Concurrency
   My reading of this document is that it does not go as far as a
   design for an implementation of Structured Concurrency
   [Sústrik2016, Smith2018].  As far as I can tell there's no notion
   in this document about parent-child relationships between fibers
   (section 2.6), nor of cancellation protocols.  That's fine!  I
   don't know if Structured Concurrency is a uniformly-desireable
   user-space programming paradigm.  But it might be interesting to
   think about how the design in this document would have to be
   extended to support Structured Concurrency efficiently.  Can it be
   done without any changes whatsoever?  Or would there need to be
   substantial changes to the scheduler, or the processes of fiber
   creation and reaping processes?  Could the scheduler design in this
   document be hookable to extend it, or does that offer too much
   flexibility for the user to accidentally degrade performance?

   Again, it's totally fine if the implementation of these is out of
   scope, or left for a future project -- but it would also be
   interesting to know what the gap is.  (And, also, whether there are
   users out there who have existing workloads that are naturally
   expressed using Structured Concurrency).
** Testing, single-threaded implementation
   Testing the implementation of fibers is almost not mentioned in
   this document at all.  Maybe there's an implicit expectation that
   there will be some suite of unit tests for the individual
   components (maybe a strong set of tests for the circular array
   implementation underlying the Lev-Chase deque, some unit-like
   testing for the deque itself, some integration tests for the deque
   along the lines of existing concurrency tests, and so on...)

   I wonder if we can imagine a way to increase confidence in testing
   faster: maybe by running the existing quite fierce regression test
   suite with various modes.  One where each =with-test= form runs on
   its own fiber?  One where =sb-thread:make-thread= runs a fiber
   instead of a thread?  Run each test file three times, on three
   fibers, divided between two carriers?  Something else along these
   lines?

   Perhaps along the same lines of thinking about unconventional ways
   of imagining running with fibers: how possible is it to imagine
   fibers of this design in a single-threaded Lisp, multiplexing
   everything (including the REPL) through a single scheduler group
   with a single carrier thread?
** Pinning, growing (and shrinking) scheduler groups
   One thing I have found useful in some contexts is to be able to
   grow thread pools -- in this context, scheduler groups (unless I've
   misunderstood).  Is this a possible (and desireable) thing to do in
   this model?

   In particular, if a fiber pins itself, that can affect the progress
   of other fibers in that scheduler group.  If enough fibers in a
   group pin themselves, then all other progress can be starved.
   Supporting growth of a scheduler group (adding cariers) on pin, as
   maybe an option, might be worth considering?  Of course if we grow
   a group, we might also need to think about shrinking too.

   On the subject of pinning: the "pin" concept in this document
   expresses both "must not be moved to another thread" and "must
   block rather than yield".  Are these two intrinsically coupled, or
   is there a case for a thread-affinity-like "must not be moved to
   another thread" while still allowing yielding?
** Performance  
   I found myself underwhelmed by the performance measurements
   presented in section 14.  (I don't really know what I expected).
   
   Is the ~1.5x improvement in HTTP request handling expected (with
   reference to fiber-like implementations in other languages, for
   example)?  Is there potential for a more noticeable tail latency
   (say 99th-percentile) improvement?

   Section 14 says that fibers provide a "decisive" advantage at high
   connection counts.  Is that actually true?  What about the
   statement about the kernel's scheduler and thousands of running
   threads (section 14)?  From the table in 14.1 it seems to me that
   the kernel scheduler is coping pretty well with 25,000 threads?
* Substantive critiques
** Unclarity through inconsistency
   The terms "scheduler" and "scheduler group" are used as distinct
   entities in various places (for example, in the terminology
   definitions in section 1.3), and conflated elsewhere.  For example
   section 3.1, but in section 2 and probably elsewhere as well.  I
   think this also has led to some lack of clarity in this document
   about the API, which entities are first-class, which entities are
   manipulable from "outside", etc.

   Is there an inconsistency between the handling of a scheduler
   group's active count?  It looks like a scheduler group wants to
   terminate when its active count is 0 (sections 8.1, 8.5) but how
   does that square with setting up a scheduler group and submitting
   fibers to it (2.7.2)?
** Debugger integration
   It seems to me that this design somewhat neglects the interaction
   between errors in fibers, the usual interactive debugger,
   signalling (including resignalling) conditions to give outer
   handlers a chance to handle or decline them; and restart-based
   protocols.

   I think it is potentially surprising to users, and it was to me, to
   find that by design in section 12 signalling a condition of type
   =error= with the =error= function in a fiber would not cause a
   debugger to be invoked.  This might be a function of different
   expectations; I can imagine that a workload of tens of thousands of
   fibers might suffer badly if they all go wrong and the session has
   tens of thousands of pending debugger sessions.  So, it might be
   that there is a constraint that means that the debugger shouldn't
   be offered, but why?  What are the tradeoffs here?

   If debugger support within fibers is possible, that would influence
   the design of guard pages for at least the control stack:
   replicating the existing support in the runtime for a debugger
   session when the (first) stack guard page is hit implies a need for
   a second stack guard page, which when hit sets up execution to
   unwind the stack completely -- along the lines of the guard page
   described in this document.
** GC correctness  
   Some of the sections of the document are substantially reference to
   code that (by the terms of this review) are out of scope;
   particularly the GC integration section.  This makes it impossible
   to assess any kind of correctness in the design.  However, at least
   the assertion in section 7.8 that the GC threads waits for all
   mutator threads to reach a safepoint is wrong in general; the GC is
   mostly not safepoint-based.  (Section 7.8 is not convincing in
   general)
** Context switching performance
   The context switch microbenchmark suggests that some of the earlier
   sections' descriptions of fiber context switching as fast needs
   revising.  The emphasis on a a "hand-written assembly routine"
   (section 1.2) for register saving is perhaps misplaced given
   everything else that needs to be done (section 3.3, which feels
   like it might have been better placed under section 4 with the rest
   of context switching.)
** Fiber results
   The document is not clear about returning results from fibers.  I
   would expect any API around return values to transparently handle
   multiple values; meanwhile, condition objects are valid values and
   must not be punned to indicate errors.  I would generally expect
   unhandled serious conditions to trigger a debugger, or failing that
   to abort in some way (and for that to cause fiber-join to signal
   some kind of condition with useful restarts).  But that's not very
   consistent with an API like run-fibers returning a list-of-results.
   On the other hand, handling multiple values straightforwardly will
   presumably involve allocation, which might not be desireable.  I
   think this whole area needs more thought, or at least it's not
   obvious to me what the right thing is.
** Chase-Lev Deque  
   The Chase-Lev Lock-Free Deque [ChaseLev2005] depends (in the small
   print) on comparison checks between top and bottom; top only ever
   increases.  If stealing dominates, at some point bottom will
   overflow.  In [ChaseLev2005] the assertion is made that 64-bit
   quantities for top and bottom give multiple years of runtime even
   with highly active stealing before overflow can occur (64 years at
   4 billion steals per second).  This is fine, and remains fine if
   these fields are implemented in terms of =fixnum= on 64-bit
   platforms (with 61 or 63 bits on PPC64 or the other 64-platforms).
   However, the document describes implementing this data structure on
   32-bit platforms, and I'm not sure that's straightforward: with the
   same steal rate assumption, the lifetime before overflow is about
   half a second, or less if using 30-bit fixnums.  (Implementing a
   64-bit field is not straightforward because top must be updated
   atomically, which means we would need double-width compare and swap
   or equivalent).

   Also on the Chase-Lev Lock-Free Deque, the implementation in
   [Lêetal2013] quotes substantial performance improvements on relaxed
   memory architectures, though note the dangers of transliterating
   algorithms from one language to another observed in this specific
   case by [Wingo2022].
* Points of detail
  - Section 1.2, "must see a consistent view of all fiber state".
    What does this mean?
  - Section 1.2, "small per-fiber memory footprint".  256KiB is only
    32 times smaller than our default 8MiB stack size; that feels like
    one order of magnitude more space, but is that obviously
    transformative?
  - Section 2.1: do we really want an additional guard page on top of
    the 256KiB stack?  Or just to =mprotect()= the last page?
  - Section 2.1: "analogous to the =:initial-bindings= argument to
    sb-thread:make-thread".  There is no such argument to
    sb-thread:make-thread.
  - Section 2.2/2.3/2.4: the =fiber-yield= / =fiber-sleep= /
    =fiber-park= lambda lists are weirdly inconsistent.  I think it
    would benefit from inversion: thinking of =fiber-park= (or
    whatever it should be called) as the primitive, and implementing
    =fiber-yield= and =fiber-sleep= in terms of it.
  - Section 2.2: "... the fiber becomes immediately runnable again (it
    is pushed back on the scheduler's deque) ...".  As described I
    think this leads to one cpu-bound fiber starving all other fibers
    on a given carrier/scheduler -- should there be some other
    auxiliary parking spot for yielding fibers which are immediately
    runnable?
  - Section 2.4: "The scheduler calls it during maintenance
    passes...".  It's perhaps important to make it explicit that the
    predicate will be called in the dynamic environment of the
    scheduler, and not the one of =fiber-park= -- so the predicate has
    indefinite extent and must capture any special bindings it wants
    to preserve.
  - Section 2.4: "Internally, =fiber-park= stores the timeout as a
    deadline... and passes the predicate to =fiber-yield=".  As
    described I think if the fiber is pinned this leaves the scheduler
    in an inconsistent state, which might matter if something handles
    the error from =fiber-yield=.
  - Section 2.6: =fiber-scheduler-group= is not in the table of API
    functions or described.  Why is it accessing the current scheduler
    rather than (as its name suggests) the current fiber?
  - Section 2.7.1: is it wise to expose the scheduler =idle-hook=
    function in a "simple" API?  There are surely so many ways to mess
    that up that I would suggest leaving it out of anything exposed as
    simple.
  - Section 2.7.2: is it legal to have a null =initial-fibers=
    argument to =start-fibers=?  Does the scheduler group instantly
    end (see section 8.1)?
  - Section 2.7.2: the argument order to =submit-fiber= feels reversed
    from what is natural to me.
  - Section 2.7.2: If I didn't understand the results of fibers now, I
    *really* don't understand how =finish-fibers= is meant to be used.
  - Section 2.7.2: I don't understand what =fiber-group-done-p= is
    for.  If it returns true, I can't take any action based on that
    because it might be false an arbitrary small amount of time later.
    If it returns false, what does that imply?  What can I not do if
    it returns false, submit new fibers?
  - Section 2.8.5: =sb-ext:wait-for= documents its timeout as waiting
    "at most approximately <timeout>".  If I've understood correctly,
    the deadline-based scheduler in this document works on an "at
    least <timeout>" basis.
  - Section 2.9: "Pinning increments a per-fiber counter".  Can
    pinning be done to fibers from outside?  The =fiber= argument to
    =fiber-pin= / =finer-unpin= / =with-fiber-pinned= suggests that it
    can.  Then the counter needs to be manipulated atomically.
  - Section 2.9: if pinning can be done from outside a fiber, I don't
    see how it is possible to use =fiber-can-yield-p= safely.
  - Section 2.9: it might be more Lispy to simply =signal= a
    documented condition, rather than the =*pinned-blocking-action*=
    cases, and allow users to handle that condition as they choose in
    their fibers.
  - Section 2.10: "The list is captured under a mutex to provide a
    consistent snapshot".  What else contends for this mutex?
    Consistent with respect to what?
  - Section 2.10: if there is =list-all-fibers= should there be
    =map-all-fibers= and/or =do-all-fibers=?
  - Section 2.10: should there be a =list-all-fibers= analogue for a
    scheduler group, as well as or instead of a global collection of
    fibers?  (What is this used for?)
  - Section 2.10: should there be an introspective function to
    determine why a fiber is =:suspended= (what it might be waiting
    for)?
  - Section 2.10: how can =fiber-alive-p= be safely used, given that
    its state might change immediately after return?
  - Section 2.10: is it reasonable to request the equivalent of
    =backtrace-as-list= for =print-fiber-backtrace=?
  - Section 2.10: what does =print-fiber-backtrace= do for fibers in
    =:running= state?
  - Section 2.10: what does =print-fiber-backtrace= do if the fiber it
    is backtracing is resumed while the backtracing is occurring?
  - Section 2.10: is there a need for intercession?  (aside from
    =fiber-pin=).  For example is it reasonable to bake in some kind
    of way to cancel fibers?
  - Section 2.11: what can I do with the return value of
    =make-fiber-scheduler=?  I think all the other API functions
    operate either on fibers or on scheduler-groups.
  - Section 3.1: "A single carrier with a single scheduler is the
    simplest configuration".  In section 1.3 "Terminology" schedulers
    are defined to have a 1:1 correspondence with carriers.
  - Section 3.3: Yield: is there a race here between steps 5 and 6,
    and things like =print-fiber-backtrace=, where the state is
    =:suspended= but not quite all of the state is saved?
  - Section 3.3: Resume: there is nothing in this set of steps that
    sets the fiber's state to anything (presumably =:running=?)
  - Section 4.1: While it might be obvious that the FPU state is
    shared between fibers in this implementation, that is
    Lisp-user-visible in a way that segment registers and signal masks
    aren't, and probably needs its own call-out for things like
    rounding modes, enabled exceptions and the like.
  - Section 4.2: why not unconditionally pass and patch the thread
    register?
  - Section 4.2: will the =ret= at the end of the sequence mispredict
    its target essentially 100% of the time?
  - Section 4.5: I think it's OK to use raw machine words but I also
    think that a VOP with sap arguments called inline with suitable
    SAP constructors should also not allocate anything.
  - Section 4.5 "TLS scratch hash table" a forward reference to
    section 6.2 would have been nice.
  - Section 5.1: Some day I will hunt down whoever it was who decided
    that downward-growing stacks should grow upward on the page, and
    give them a piece of my mind.
  - Section 5.2: calling =lose()= from hitting a guard page -- as
    opposed to "just" killing the fiber -- is excessive, isn't it?
    Can we do better?
  - Section 5.3: "singly-linked free list protected by a mutex": for
    some fiber workloads, what is the expected contention here?
  - Section 5.5: I don't see why relocating the stack would invalidate
    return addresses.  (I agree that given imprecise heap references
    on the stack relocation is not possible).
  - Section 6.1: "This create a problem" is missing an "s" on
    "create".
  - Section 6.2: Pass 1 could perhaps be better done with a
    bit-vector?  If we are avoiding consing, then a pre-allocated
    bit-vector capable of storing a bit for each of the possible TLS
    indices will be much smaller than a pre-allocated hash-table
    capable of doing this.
  - Section 6.4: =handler-case= is a much higher-level construct than
    =throw= and =unwind-protect=: if catch and unwind save/restore are
    working, I would not expect anything extra to be done to support
    any of the condition system (=signal= / =handler-bind= /
    =restart-bind=) can be implemented in pure CL [Pitman1988].
  - Section 7.7: =pseudo-atomic= is a further mechanism for inhibiting
    GC for short sequences of machine instructions.
  - Section 7.7: having a large =without-interrupts= block stops
    interactive debugging from SIGINT or related signals, or even
    stack traces from errors.  This might affect the debuggability of
    the critical parts.  (That might be OK)
  - Section 7.7: is it in fact the case that =without-interrupts=
    blocks the stop-for-GC signal?  I think this might be a
    misunderstanding.
  - Section 7.8: can the inconsistent state be observed by anything
    else?  (introspective tools?)
  - Section 8.2: should the period-64 maintenance cycle be a
    parameter?  Or adaptive in some way?
  - Section 8.4: after calling =destroy-fiber=, what happens to the
    fiber's result value(s)?  How does this interact with returning
    values?
  - Section 8.5: the 1ms timeout seems arbitrary here.  Is it a
    parameter?  Should there be some kind of backoff?  Why is there a
    need for a timeout at all?
  - Section 8.6: "64 is a compromise: at 2 million switches/sec".
    Where does the 2 million switches per second come from?  If it's
    from the measurement in section 14.4, that value is an upper bound
    (so the quoted ~32 microseconds is a lower bound).  What is the
    value for more typical workloads, and how does that affect the
    provision of "sub-millisecond responsiveness"?
  - Section 9.1: =top= is in general written by the owner (via CAS) as
    well as by thieves.  It is also worth making explicit here that
    =top= is only ever incremented, never decremented.
  - Section 9.4: I know it's this way in the literature, but just as
    with stacks and their diagrams I will regret =top= being below
    =bottom= and how nonsensical that makes the =loop= bounds seem.
    (I believe they're correct).
  - Section 9.4: The sentence "The owner publishes the new buffer..."
    is unclear, but I think the point is that the new buffer is
    separate from the =top= and =bottom= values, and thieves can in
    principle steal all the active entries while the buffer growth is
    occurring because of the atomic incrementing of =top=.  (The word
    "harmlessly" should perhaps be "correctly"?)
  - Section 9.4: Is there a case for shrinking the circular arrays if
    they are underpopulated?
  - Section 9.5: =random= might be too heavyweight for this, at least
    with the current MT19937-based implementation.  Are there
    alternatives?  (last-successfully-stolen-index + self-index mod n,
    maybe?)
  - Section 10.5: I didn't understand "with a fast-path check against
    the scheduler's =ready-fds= hash table on Linux".
  - Section 10.6: The 100ms and 10ms values for caps here seem
    arbitrary.  Should they be parameterized?  Why are they different?
  - Section 10.7: given that we have a hash table of fds to waiters
    (section 10.3), why is the single poll O(W)?  Would it not be
    O(FD) instead?
  - Section 12.1: I'm not convinced that even if we want to not
    support interactive debugging, this is the best way of writing
    that.  It interferes with handlers resignalling outwards before
    deciding to handle; it doesn't set up useful restarts; more
    prosaically it doesn't handle =serious-condition=.  (Also, I think
    it would be kind if it bound a handler that attempted to call
    =continue= before abandoning entirely).  I agree with the design
    goal in section 12.2 that an unhandled "user-mode" condition in
    one fiber should not take the whole carrier down without explicit
    user intervention, but maybe these decisions are better expressed
    using restarts?
  - Section 12.4: "the fiber's =gc_info= is widened to cover the full
    stack".  Is there something that ensures that the unused part of
    the fiber stack is zeroed, or might the GC see old data at that
    point?
  - Section 13.7: =interrupt-thread= is fundamentally broken and
    should not be used.  "on carrier threads" is superfluous.
  - Section 14.2: the table is, well, not very interesting.  I think
    information about how the system behaves with different values for
    thread control stack size (controllable by the
    =--control-stack-size= command-line argument to SBCL) would be
    more illuminating.
  - Section 14.3: if the GC pause times scale linearly, as is
    expected, what is the coefficient?  (And how does it compare to an
    equivalent number of threads?)
  - Section 14.4: please use the same units for both rows in this table.
* Metareview
** Positives
   I was far from an expert in user-mode threading before starting
   this review.  (For the avoidance of doubt: I'm still far from an
   expert).  This process has forced me to read, think and learn, and
   has probably given me an improved understanding of some of the
   programming frameworks that I interact with (and have to debug) in
   the course of my work.

   Also, to the extent that this is a test-bed for assessing
   LLM-assisted (at the very least) design, I found the document to be
   not excessively alienating.  I don't know exactly the process in
   getting it to the form in which I received it, but whatever it was
   it ended up being of an comparable quality to documents I have read
   in the past -- for the avoidance of doubt, that's a wide range:
   I've read some truly awful student project reports and conference
   paper submissions, as well as some glorious scientific work.
** Annoyances
   I have found the process of engaging with this document difficult
   along three axes, which is two more than the usual.  Because of the
   process that generated it, and the use that it might be put to, I
   have felt that:

   1. Each sentence needs to be read in detail and with suspicion,
      because there is a reasonable chance that some fraction of the
      facts asserted within the document will be wrong.
   2. Each design decision needs to be read in detail and considered
      through the lens of a perverse implementor with limited cultural
      context.
   3. Any feedback that I give might end up being shoved into a
      context-free process for distillation before being considered by
      any humans.

   I have experience with (1) from my past life as an educator: this
   part is not unlike reading student work, or indeed research paper
   submissions.  I have experience with (2) from work in my current
   position, where critiquing design documents written by people who
   might not be the same people as will eventually implement the
   design is relatively rare but not unknown in my experience.  Each
   of (1) and (2) I can work with; both at once is a bit painful, but
   not unlike working with interns, Summer of Code students, or the
   like.

   Unfortunately I also have growing experience with (3); the less
   said about that the better.

   All three at once is tiring!  I think I'd recommend for us to guide
   newcomers to avoid triggering all three of these concerns at once.
   The easiest one to alleviate, I would suggest, is (3), and I would
   encourage people to open discussions early: commit to frequent,
   repeated interactions to build trust that those interactions are
   rewarding to all parties.  In the LLM world, (2) is perhaps
   unavoidable; although best practice [Willison2026] might include
   thoroughly reviewing code before sending patches or pointing to
   branches, in practice any LLM-authored patch coming from a design
   document like this will have to be read in fine detail.

   Can we alleviate (1)?  The superficial polish of LLM-authored
   prose, with its tendency towards persuasive writing, coupled with
   the nature to produce content of similar shape to the training set,
   makes it likely that we will be dealing with plausible falsehoods
   (or "hallucinations"; see [Broad2026] for a recent example) and the
   consequences of them (such as [Xkcd978]) for the forseeable future.
   I acknowledge that there's a continuum of tools to support writing,
   from spell checkers through automated translation to style
   assistance, generative systems and beyond; I don't know what the
   answer is here.  (Perhaps once again it boils down to trust and
   understanding, which can only be built through repeated positive
   interaction).

*** Addendum
    After finishing this review, I showed it to a colleague.  They
    pointed out to me that the 8 MB per-thread stack size quoted
    throughout the document, in section 1 (motivation), 5 (design) and
    14 (measurements, if multiplying numbers is a measurement) is not
    correct.  I offer this as yet another datum: despite my being
    primed to suspect every claim in this document, and aiming to read
    the document as cautiously as possible, the value quoted looked
    plausible, was used consistently, and its plausibility allowed
    that value to just slide by.

* References
  - Broad2026 :: Why we should care about ChatGPT's accuracy gap.  https://leahbroad.substack.com/p/chatgpts-accuracy-gap-is-dangerous
  - ChaseLev2005 :: Dynamic Circular Work-Stealing Deque, 2005.  doi:10.1145/1073970.1073974
  - LêEtAl2013 :: Correct and Efficient Work-Stealing for Weak Memory Models. 2013.  doi:10.1145/2442516.2442524
  - Pitman1988 :: CONDITIONS.  https://www.nhplace.com/kent/CL/Revision-18.lisp
  - Smith2018 :: Notes on structured concurrency, or: Go statement considered harmful.  https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
  - Sústrik2016 :: Structured Concurrency.  https://www.250bpm.com/p/structured-concurrency
  - Willison2026 :: Agentic Engineering Patterns.  https://simonwillison.net/guides/agentic-engineering-patterns/
  - Wingo2022 :: on "Correct and Efficient Work-Stealing for Weak Memory Models".  https://wingolog.org/archives/2022/10/03/on-correct-and-efficient-work-stealing-for-weak-memory-models
  - Xkcd978 :: Citogenesis.  https://xkcd.com/978/