{"article":{"slug":"concurrentml-improving-on-gos-concurrency-model","title":"ConcurrentML: Improving on Go's concurrency model","subtitle":null,"summary":"Nikhil explores Concurrent ML-style first-class events as a richer concurrency model than Go's channels alone: wrapping and composing communication operations, selective choice, guard events, and cancellation, with Racket ConcurrentML as the reference and notes on how Go falls short.","content_type":"essay","language":"en","canonical_url":"https://nikhilism.com/post/2026/golang-racket-concurrency/","author":{"name":"Nikhil","url":"https://nikhilism.com/","person_slug":null,"person_url":null},"authored_by":"human_and_agent","publisher":{"name":"Nikhil's blog","url":"https://nikhilism.com/","listing_slug":null,"listing":null},"topics":[{"name":"Programming Languages","slug":"programming-languages","url":"https://listedarticles.com/topics/programming-languages"},{"name":"Systems Programming","slug":"systems-programming","url":"https://listedarticles.com/topics/systems-programming"},{"name":"Software Engineering","slug":"software-engineering","url":"https://listedarticles.com/topics/software-engineering"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":6476,"reading_minutes":28,"published_at":"2026-10-16T14:09:00.000Z","added_at":"2026-10-21T08:12:58.778Z","updated_at":"2026-10-21T08:12:58.778Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/concurrentml-improving-on-gos-concurrency-model","markdown_url":"https://listedarticles.com/articles/concurrentml-improving-on-gos-concurrency-model.md","example":false,"citation":"Nikhil, Nikhil's blog. \"ConcurrentML: Improving on Go's concurrency model.\" 16 Oct 2026. https://nikhilism.com/post/2026/golang-racket-concurrency/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://nikhilism.com/post/2026/golang-racket-concurrency/"},"body_markdown":"# ConcurrentML: Improving on Go's concurrency model\n\nPosted on Oct 16, 2026\n\n- [events: communication operations as values](#events-communication-operations-as-values)\n  - [Wrapping events](#wrapping-events)\n  - [Composing choice](#composing-choice)\n- [Building on events](#building-on-events)\n  - [Guard events](#guard-events)\n  - [Handling cancellation](#handling-cancellation)\n  - [Cancellation in the print service](#cancellation-in-the-print-service)\n- [Wrapping up loose threads](#wrapping-up-loose-threads)\n- [`close`ing thoughts](#closeing-thoughts)\n- [References](#references)\n- [Appendix: Events beyond channels](#appendix-events-beyond-channels)\n\n***LLM use disclosure**: This post is primarily written by a human. However, I've used LLMs for speech-to-text, basic\ngrammar correction, feedback about the writing and for adding inline API documentation links automatically.*\n\nThe Go programming language was instrumental in bringing the Communicating Sequential Processes (CSP) concurrency model\ninto widespread use. In the CSP model, channels are usually *unbuffered* and act not just as a means of communication,\nbut also for synchronization. That is, sending or receiving blocks until the other side is ready, and then the value is moved from one thread to another atomically.\n\nIn addition, the [`select`](https://go.dev/ref/spec#Select_statements) statement guarantees that out of all the channel\noperations in its clauses, only one is selected and the others remain unprocessed. This allows several\nmessaging patterns to be expressed succinctly.\n\nCSP is an [old idea](https://web.archive.org/web/20260922190027/https://www.cs.cmu.edu/~crary/819-f09/Hoare78.pdf \"Hoare's original paper\") (from the 1970s), and while Go sticks relatively closely to that model, other languages have taken it\nfurther. In my opinion, the most interesting one is Concurrent ML (CML). Introduced in the mid-1990s, CML still has much to teach modern language designers.\n\nIn this post, I want to argue that Concurrent ML’s developments on top of CSP offer compelling advantages for implementing concurrent systems, fixing several of Go’s deficiencies.\n\n**CML’s key idea is to promote a communication operation from an executable statement to a *value* describing the operation. The execution of that description can be done separately.** Don’t worry if this isn’t clear right away, this whole essay is an elaboration of that idea.\n\nThis unlocks several powerful improvements over Go/CSP:\n\n1. It unifies the concepts of choice (what `select` does) and cancellation into a unified, general abstraction.\n2. This abstraction enables composition and encapsulation of concurrency, which is impossible in Go.\n\nSeparately, CML fixes one of Go’s biggest concurrency footguns – goroutines leak unless properly shutdown – via straightforward garbage collection.\n\nWhile the ideas come from CML, today [Racket](https://racket-lang.org) is the programming language that best espouses them.\nRacket is actively developed, and the CML model is comprehensively supported and used within it. With that in mind, I will be demonstrating all examples in Racket and will interchangeably\nuse the terms CML or Racket based on context.\n\nSimilarly, I will use the words *goroutine* and *thread* interchangeably. Their semantics are identical, at least for the\npurposes of this article.\n\nThis article does not propose anything new. It is meant to introduce practitioners to a model that has generally\nremained in the academic community. This article assumes that you are reasonably familiar with Go\nsyntax and the behavior of its channels. It does not assume familiarity directly with Racket, but you should be able to\nfollow basic ideas about function calls and recursion in a Scheme-like language.\n\nTo try out the Racket code snippets, [download Racket](https://download.racket-lang.org/), fire up the DrRacket editor, and paste the examples in the main window. Press the Run button to execute.\n\n## events: communication operations as values\n\nIn Go, if `ch` is a channel, the following two operations are used to send or receive messages on the channel.\n\n```\n1// Don't run this, it will deadlock\n2ch := make(chan int)\n3ch <- 5\n4x := <-ch\n```\n\nThese channel operation statements [take effect immediately when reached](https://go.dev/tour/concurrency/2).\n\nWhen used with the [`select`](https://go.dev/ref/spec#Select_statements) statement, these expressions can be combined in `case` clauses to express\npotential choices that the runtime can select between. When execution reaches a [`select`](https://go.dev/ref/spec#Select_statements) statement, all channel operations are\npolled to see which ones are ready to execute. One operation is picked from those that are ready. It is\nguaranteed that no other channel operation will be completed within this execution.\n\n```\n1select {\n2case ch1 <- 5:\n3case y := <- ch2:\n4default:\n5\t// selected if none of the others are ready.\n6}\n```\n\nWithin the select, channel operations act as *descriptions*, which the `select` statement uses to determine which channels to poll, and for what operation. That is, hitting the first case does not immediately block the entirety of the `select`. However, in Go, this descriptive form cannot be decoupled from the [`select`](https://go.dev/ref/spec#Select_statements) statement and stand on its own. As soon as you try that, it becomes the previous example, and the statement tries to execute immediately. This prohibits composition and encapsulation. A function cannot return a description which you can choose to `select` on later. Nor can you stash the description within a struct for carrying around.\n\nChannels in Racket start of the same way and provide direct analogs of the blocking send and receive operations.\n\n```\n1; ch := make(chan int)\n2(define ch (make-channel))\n3; ch <- 5\n4(channel-put ch 5)\n5; x := <-ch\n6(define x (channel-get ch))\n```\n\nHowever, they also offer a standalone way to describe the operation.\n\n```\n1; sort of like `case ch1 <- 5`.\n2(channel-put-evt ch 5)\n3\n4; There is no channel-get-evt.\n5; Instead a channel itself can directly be used as a description of the ability\n6; to receive from it.\n```\n\n**This is an expression returning a value *NOT* a statement of action!**\n\n`channel-put-event` **returns a value, which is called an *event*** in CML. It does not immediately send the value 5 on the channel.\n\nThe event can be executed later to actually apply its effect. Because it is a value, just like the number 5, we can do a lot of useful things with it, which are not possible in Go:\n\n- assign it to a variable,\n- return it from a function,\n- put it into a struct,\n- abstract the fact that we are performing a channel operation behind a regular function, and\n- wrap it with additional concurrency combinators to express patterns\n\nIn addition, events are more general than channel operations, being able to express I/O operations that are commonly exposed by “event loops” from other languages.\n\nThe event’s actual effects are put into motion by the [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29) function\n\n```\n1(sync (channel-put-evt ch 5))\n```\n\nUsing [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29) *attempts to synchronize* a set of events. Just like `select`, all events passed to it are polled. An event that is *ready* is selected, and `sync` returns the *synchronization result* of the selected event, which is something each event can define. For sending on channels, this result is the event itself. When receiving from a channel it is the value received from the channel.\n\nWhen multiple events are ready, one is chosen randomly, just like Go.\n\nFor example, here is a Racket thread that calculates the sum of a list of numbers and responds on a channel.\n\n```\n 1#lang racket/base\n 2(define (sum items resp-ch)\n 3  (define total (apply + items))\n 4  (channel-put resp-ch total))\n 5\n 6(define ch (make-channel))\n 7(define the-numbers (list 7 2 8 -9 4 0))\n 8\n 9(thread (λ () (sum the-numbers ch)))\n10\n11(define total (sync ch))\n12(printf \"Sum of ~a is ~a\" the-numbers total)\n```\n\nNote that this separation of description and execution can easily be recombined. [`channel-put`](https://docs.racket-lang.org/reference/channel.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._channel-put%29%29) can be defined as:\n\n```\n1(define (channel-put ch val)\n2  (sync (channel-put-evt ch val)))\n```\n\n### Wrapping events\n\n> Throughout these essay, we will be using lots of anonymous functions. In Go, they are created with `func`. In Racket, the [`lambda`](https://docs.racket-lang.org/reference/lambda.html#%28form._%28%28lib._racket%2Fprivate%2Fbase..rkt%29._lambda%29%29) form, or literally [`λ`](https://docs.racket-lang.org/reference/lambda.html#%28form._%28%28lib._racket%2Fprivate%2Fbase..rkt%29._lambda%29%29) is used. If you want to refresh your knowledge of these, see [this](https://go.dev/doc/codewalk/functions/) for Go or [this](https://docs.racket-lang.org/guide/syntax-overview.html#%28part._.Anonymous_.Functions_with_lambda%29) for Racket.\n\nThe [first](https://go.dev/tour/concurrency/5) example of the [`select`](https://go.dev/ref/spec#Select_statements) statement in the Tour of Go uses a goroutine to compute Fibonacci numbers, and relays the response using a channel. It also uses an additional channel to tell the goroutine to quit. This is the Racket translation:\n\n```\n 1#lang racket/base\n 2(define (fibonacci out-ch quit-ch)\n 3  (let loop ([x 0]\n 4             [y 1])\n 5    (sync\n 6     (handle-evt (channel-put-evt out-ch x) (λ (_) (loop y (+ x y))))\n 7     (handle-evt quit-ch (λ (_) (displayln \"quit\"))))))\n 8\n 9(module+ main\n10  (define c (make-channel))\n11  (define quit-ch (make-channel))\n12  (thread\n13   (λ ()\n14     (for ([i (in-range 10)])\n15       (displayln (channel-get c)))\n16     (channel-put quit-ch #f)))\n17  (fibonacci c quit-ch))\n```\n\nThere are some functional-language-specific forms like the use of a named\n`let` to recursively express the `for {}` infinite loop. `in-range` is Racket’s number iteration form, so the `for` loops 10 times, retreiving the first 10 Fibonacci numbers. Just like [`select`](https://go.dev/ref/spec#Select_statements) accepts several channel operations,\n[`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29) accepts\nmultiple events, and then blocks until at least one of them is ready for synchronization.\n\nThis first example introduces a new function [`handle-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._handle-evt%29%29). This is an *event combinator* - a function that augments events, and itself return an event.\n\nIn Go’s [`select`](https://go.dev/ref/spec#Select_statements), a `case` clause can be followed by a series of statements, which are executed iff that clause was selected and the operation executed. You can think of the [`select`](https://go.dev/ref/spec#Select_statements) clauses as being *commit points* where a specific communication operation gets committed to, and others are abandoned.\n\nWith this mental model, we can do things before a commit point (*setup*) or after a commit point (*post-commitment*).\n\nOnce we start composing events, it no longer makes sense to associate setup and post-commitment actions with\nthe [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29), otherwise, whenever you returned an event from a function, you would also need to return the\nsetup actions separately. Instead, we can attach such steps to the event itself. This is done through a wrapper\nfunction that takes an event and a function. Each wrapper augments the event in a specific way. These are also called *event combinators*.\n\nOur first wrapper is [`handle-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._handle-evt%29%29), which associates *post-commitment* actions with an event. It returns a new, wrapped event. When the wrapped event\ngets selected by a [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29) (i.e. it wins the race), the associated function is run with the synchronization result of the event. In the example above, we use these actions to decide whether to continue looping or quit.\n\nNote that these wrappers are taking advantage of the general nature of events to put many common actions within that framework. All wrappers are encapsulating their input events, augmenting them, and returning output events. Users of those events *cannot* and *need not* figure out what is hiding behind the event.\n\nAs another example of encapsulation, the quit *pattern* can be pulled into a `quit-evt`. Nothing outside the implementation needs to know that the quit is actually implemented by channels.\n\n```\n 1#lang racket/base\n 2; returns a quit-evt, and a function to trigger the quit.\n 3(define (make-quit-evt)\n 4  (define my-quit-ch (make-channel))\n 5  ; our quit-evt in this case is just a channel, but nobody needs to know that.\n 6  ; values is how a function returns multiple values in Racket.\n 7  (values my-quit-ch (λ () (channel-put my-quit-ch #f))))\n 8\n 9(define (fibonacci out-ch quit-evt)\n10  (let loop ([x 0]\n11             [y 1])\n12    ; It can later be used here.\n13    (sync (handle-evt (channel-put-evt out-ch x) (λ (_) (loop y (+ x y))))\n14          quit-evt)))\n15\n16(module+ main\n17  (define c (make-channel))\n18  ; define-values is an \"unpacking\" operation for a function returning multiple values.\n19  ; Similar to how result, err := Func() is used in Go.\n20  (define-values ( quit-evt quit-fn) (make-quit-evt))\n21  (thread (λ ()\n22            (for ([i (in-range 10)])\n23              (displayln (channel-get c)))\n24            (quit-fn)))\n25  (fibonacci c quit-evt))\n```\n\nI hope it has become clear that treating channel operations as descriptive values has unlocked composition!\n\n### Composing choice\n\nThe ability to return events means that Racket code often builds up a list of events from various sources and then calls [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29) on all of them. [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29) is actually performing a *choice* operation between those events, and Racket allows that concept to also be separated into a wrapper event.\n\nThe combinator is [`choice-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._choice-evt%29%29).\n\nHere is a common pattern of “fetch data from 2 different sources, and accept whichever one returns the fastest”.\nWe can easily compose it with timeouts. There isn’t a [`context.Context`](https://pkg.go.dev/context#Context) in sight!\n\n```\n 1#lang racket/base\n 2\n 3; This timeout-evt is used throughout the rest of the essay.\n 4; It translates a relative time to absolute time and uses it with the built-in alarm-evt.\n 5(define (timeout-evt msec)\n 6  (alarm-evt (+ (current-inexact-monotonic-milliseconds) msec) #t))\n 7\n 8; APIs are made-up\n 9(define (http-get-evt url)\n10    ...)\n11\n12(define (fetch-from-fastest-url url1 url2)\n13  (choice-evt (http-get-evt url1)\n14              (http-get-evt url2)))\n15\n16; Perform 2 http requests, returning the result of whichever responded fastest, but also capped with a timeout.\n17(sync (fetch-from-fastest-url url1 url2)\n18      (timeout-evt 5))\n```\n\nThis sort of composition is *impossible* to express in Go, because it is not possible to wrap base operations or choices over them into re-usable functions. Spawning goroutines that return channels *is not the same*. In Racket, the *actual fetch operation does not start before `sync` is called*!\n\nI again want to stress that this composition of concurrency is a *really big deal*. Library code can assemble local concerns like choosing between two events, but let callers decide when to execute the operations, and callers themselves can impose their own rules, like timeouts, with neither needing to accomodate the other.\n\n## Building on events\n\nFor the rest of this post, we will use a single running example to see how CML builds on the basic primitive of events to represent various patterns. We will discover how this system lends itself to a slightly different style of writing programs. It is *almost* an inversion of the way Go programs are written:\n\n1. In CML, it is common to spawn threads *within* library APIs. In contrast, idiomatic Go libraries generally recommends asking callers to spawn a goroutine if they want concurrency.\n2. Cancellation is *built into the system* instead of layered on top with [`context.Context`](https://pkg.go.dev/context#Context). [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29) is aware of the cancellation APIs and actively participates in them.\n\nIn both languages, it is common to model a service that mediates access to a singleton\nresource [using a dedicated goroutine](https://gobyexample.com/stateful-goroutines). Say a *print service*, powering a [hardware printer](https://en.wikipedia.org/wiki/Printer_(computing)). Channels are used to send requests to the goroutine,\nwhich loops indefinitely. If the service accepts the channel request, then it *will* eventually\nsend the appropriate commands to the machine. It can perform queuing, and handle paper running out and so on, while keeping track of pending jobs. Obviously, in our examples, we will just `print` out to stdout.\n\nWe also want to abstract the fact that the service is running in another thread, or even that it communicates via\nchannels. Obviously, this is ridiculous in such a tiny example, but bear with me here. Here is the simplest translation of these requirements to Go:\n\n```\n 1package main\n 2\n 3import (\n 4\t\"fmt\"\n 5\t\"time\"\n 6)\n 7\n 8type PrintService struct {\n 9\tprintCh chan string\n10}\n11\n12func MakePrintService() PrintService {\n13\tprintCh := make(chan string)\n14\tgo func() {\n15\t\tvar req string\n16\t\tfor {\n17\t\t\treq = <-printCh\n18\t\t\tfmt.Printf(\"Got print request: \\\"%s\\\"\\n\", req)\n19\t\t\t// Uncomment this to simulate a slow service.\n20\t\t\t// time.Sleep(3 * time.Second)\n21\t\t}\n22\n23\t}()\n24\treturn PrintService{\n25\t\tprintCh: printCh,\n26\t}\n27}\n28\n29func (ps *PrintService) Print(msg string) {\n30\tps.printCh <- msg\n31}\n32\n33func main() {\n34\tservice := MakePrintService()\n35\tfor _ = range 2 {\n36\t\tservice.Print(\"Hello World\")\n37\t\ttime.Sleep(100 * time.Millisecond)\n38\t}\n39}\n```\n\nRacket `struct`ures are similar to Go, just with different syntax:\n\n```\n1; A struct is defined like this.\n2(struct person (name age))\n3\n4; Instances are constructed like this\n5(define nikhil (person \"Nikhil\" 2))\n6\n7; Fields are accessed using getters of the form <struct name>-<field name>\n8(printf \"Hello, ~a!\" (person-name nikhil))\n```\n\nand the service:\n\n```\n 1#lang racket/base\n 2\n 3(struct print-service (print-ch))\n 4\n 5(define (make-print-service)\n 6  (define (server-loop print-ch)\n 7    (let loop ()\n 8      (define req (channel-get print-ch))\n 9      (printf \"Got print request: ~a~n\" req)\n10      ; (sleep 3)\n11      (loop)))\n12\n13  (define print-ch (make-channel))\n14  (thread (λ () (server-loop print-ch)))\n15\n16  (print-service print-ch))\n17\n18(define (print-evt service msg)\n19  (channel-put-evt (print-service-print-ch service) msg))\n20\n21(module+ main\n22  (define service (make-print-service))\n23  (for ([i (in-range 2)])\n24    (sync (print-evt service \"Hello World\"))))\n```\n\n### Guard events\n\nA new requirement comes in that we should show the user whether the print succeeded. We need a way for the service to\nrespond to each print request. In both languages, [we can make a new channel per request to relay the response](https://go.dev/doc/effective_go#chan_of_chan). The\nchannel is sent to the service along with the request. Here is the Go code, with relevant changes highlighted:\n\n```\n 1package main\n 2\n 3\n 4import (\n 5\t\"fmt\"\n 6\t\"time\"\n 7)\n 8\n 9type PrintRequest struct {\n10\tmsg    string\n11\trespCh chan string\n12}\n13\n14type PrintService struct {\n15\tprintCh chan PrintRequest\n16}\n17\n18func MakePrintService() PrintService {\n19\tprintCh := make(chan PrintRequest)\n20\tgo func() {\n21\t\tvar req PrintRequest\n22\t\tfor {\n23\t\t\treq = <-printCh\n24\t\t\tfmt.Printf(\"Got print request: \\\"%s\\\"\\n\", req.msg)\n25\t\t\t// Uncomment this to simulate a slow service.\n26\t\t\t// time.Sleep(3 * time.Second)\n27\t\t\ttime.Sleep(500 * time.Millisecond)\n28\t\t\treq.respCh <- \"success\"\n29\t\t}\n30\n31\t}()\n32\treturn PrintService{\n33\t\tprintCh: printCh,\n34\t}\n35}\n36\n37func (ps *PrintService) Print(msg string) string {\n38\trespCh := make(chan string, 1)\n39\tps.printCh <- PrintRequest{msg: msg, respCh: respCh}\n40\treturn <-respCh\n41}\n42\n43func main() {\n44\tservice := MakePrintService()\n45\n46\tfor _ = range 2 {\n47\t\tresp := service.Print(\"Hello World\")\n48\t\tfmt.Printf(\"PrintService response: %s\\n\", resp)\n49\t}\n50}\n```\n\nThis `Print()` does three different things. First, it mints a new response channel. This is the *setup* phase. Then,\nit sends the request and awaits the response.\n\nTurning to Racket, how would we achieve this? Where [`handle-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._handle-evt%29%29) was an event combinator to express *post-commitment* actions, there is a dual combinator [`guard-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._guard-evt%29%29) for expressing *setup* actions.\n\n[`guard-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._guard-evt%29%29) is an event *maker*. It is a function that accepts a function, stores it somewhere, and returns an event. We will call this returned event a *guard event* to distinguish it clearly. When the guard event is *actually used* in\na [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29), it does something special: It invokes the stored function, and expects the function to return an\nevent[1](#fn:1). Then, the guard event “replaces” itself with the returned event. This naturally expresses\nthe setup phase.\n\n```\n 1#lang racket/base\n 2\n 3(define my-evt\n 4  (guard-evt\n 5   (lambda ()\n 6     (println \"I will be printed only when used in a sync\")\n 7     ; A special built-in event that is never ready for synchronization.\n 8     never-evt)))\n 9\n10(define my-evt2\n11  (guard-evt\n12   (lambda ()\n13     (println \"I will be printed only when used in a sync\")\n14     (timeout-evt 200))))\n15\n16; no output yet.\n17\n18(sync my-evt my-evt2)\n19; Prints \"I will be printed only when used in a sync\" **twice**!\n```\n\nComing back to our print-service, here is how we can use [`guard-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._guard-evt%29%29) to create the response channel:\n\n```\n 1#lang racket/base\n 2(require racket/async-channel)\n 3\n 4(struct print-service (print-ch))\n 5(struct print-req (msg resp-ch))\n 6\n 7(define (make-print-service)\n 8  (define (server-loop print-ch)\n 9    (let loop ()\n10      (define req (channel-get print-ch))\n11      (printf \"Got print request: ~a~n\" (print-req-msg req))\n12      (sleep 0.5)\n13      (async-channel-put (print-req-resp-ch req) 'success)\n14      (loop)))\n15\n16  (define print-ch (make-channel))\n17  (thread (λ () (server-loop print-ch)))\n18  (print-service print-ch))\n19\n20(define (print-evt service msg)\n21  (guard-evt (λ ()\n22               (define resp-ch (make-async-channel 1))\n23               (channel-put (print-service-print-ch service)\n24                            (print-req msg resp-ch))\n25               resp-ch)))\n26\n27(module+ main\n28  (define service (make-print-service))\n29  (for ([i (in-range 2)])\n30    (sync (handle-evt (print-evt service \"Hello World\")\n31                      (λ (resp) (printf \"Printer response: ~a~n\" resp))))))\n```\n\nWe introduce the struct `print-req` that will hold the request details. `print-evt` returns a *guard* event obtained from `guard-evt`.\nWhen called as a function, the lambda passed to `guard-evt` is not immediately invoked. Once `sync` starts executing at line 30, the lambda is invoked, and the set of events the `sync` is monitoring now contains `resp-ch`.\n\n### Handling cancellation\n\nSo far we have demonstrated:\n\n1. How events are descriptions of actions, and their effects only execute when used in [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29).\n2. How the CML model uses various combinators to frame setup and post-commitment actions.\n\nWe will now see how these features are combined to tackle the problem of cancellation/abandonment. In\nconcurrent systems, we often want to cancel some action because doing so will save resources or avoid some side effect. [2](#fn:2)\n\nCommon reasons are:\n\n1. We are doing an action that could take unbounded time and we want to abandon it on timeout, or\n2. The user or client is no longer interested in the result of the action, possibly because *some* other action succeeded.\n\nIn Go, the cancellation mechanism is established by convention. Functions that perform cancellable actions accept a\n[`context.Context`](https://pkg.go.dev/context#Context), and monitor its [`Done()`](https://pkg.go.dev/context#Context.Done) channel concurrently with other actions (via [`select`](https://go.dev/ref/spec#Select_statements)). On the caller side,\nhelpers like [`WithTimeout`](https://pkg.go.dev/context#WithTimeout) or [`WithCancel`](https://pkg.go.dev/context#WithCancel) are used to close the channel backing [`Done()`](https://pkg.go.dev/context#Context.Done).\n\nCML sees the hammer of events, and finds nails everywhere! Cancellation is expressed through *negative acknowledgement events* (*nack* events). *Unlike Go, this system is baked into the runtime.*\n\n*The rest of this section may seem complicated or hard to follow, so it will help to remember that cancellation is the aspect in which CML is closest to Go.* In both, there is an event (`ctx.Done()` in Go) that must be monitored to detect cancellation. It is only a conventional difference in where and how the event is created. In Go, `Context` is created at the top-level, and threaded through every function. CML sticks to the compositional advantages of events and event-combinators. Cancellation is opted into by individual events, by using a special guard combinator, and callers are none-the-wiser.\n\nAs a quick refresher, here is how cancellation is expressed in Go using `Context`. Each function in the call heirarchy can add its own cancellation conditions, with custom timeouts, or direct calls to `cancel()`.\n\n```\n 1func DoMoreStuff(ctx context.Context) {\n 2  ...\n 3}\n 4\n 5func DoStuff(ctx context.Context) {\n 6  ctx, cancel := context.WithTimeout(ctx, 1*time.Second)\n 7  defer cancel()  // Don't forget this.\n 8  select {\n 9        op1:\n10            // do things\n11        op2:\n12            // do other things\n13  }\n14  // Whichever op is not selected can cancel based on monitoring Done()\n15}\n```\n\nIn Racket, [`nack-guard-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._nack-guard-evt%29%29) is the combinator that manufactures a negative acknowledgement (henceforth, *nack*) event. True to its name, it is [`guard-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._guard-evt%29%29) with a slight twist. The maker function receives a special event as the first argument, like so:\n\n```\n1#lang racket/base\n2(nack-guard-evt (lambda (nack-evt)\n3  ...))\n```\n\nJust like `guard-evt`, the maker function should return the actual event to be used in a `sync`. [`nack-guard-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._nack-guard-evt%29%29) establishes a one-to-one association between the nack event and the event returned from the maker function. Note that there are 3 different events to keep track of here, so let’s name them:\n\n1. The event returned by `nack-guard-event` itself, which we will call the *guard event*.\n2. The *original event* that the maker function will return, which will be a channel or similar.\n3. The `nack-evt` that was created by `nack-guard-evt` and passed to the maker function.\n\nThe guard event is acting as a proxy for the original event, so, when used in a [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29), three things can happen:\n\n1. The original event is *ready and gets picked by `sync`.* In this case, `nack-evt` will *never* become ready.\n2. The original event *does not get picked* by `sync`. In this case, `nack-evt` *will* become ready.\n3. The `sync` is interrupted, the thread is killed or something throws an exception. `nack-evt` becomes ready in this case too!\n\nNow, something else in the program needs to monitor the `nack-evt`’s readiness. This is conventionally done by spawning a separate thread to `sync` on that and *the actual operation that will lead to original event becoming ready*.\n\nHere is a quick example to make things clear.\n\n```\n1#lang racket/base\n2(define ch (make-channel))\n3\n4(sync (nack-guard-evt (lambda (nack-evt)\n5                        (thread (lambda ()\n6                                  (sync nack-evt)\n7                                  (println \"nack-evt became ready!\")))\n8                        (channel-put-evt ch \"hello\")))\n9      (timeout-evt 2000))\n```\n\nWe wrap a `channel-put-evt` with `nack-guard-evt`. That is synchronized along with a `timeout-evt`.\nWhen execution reaches the `sync` then `nack-guard-evt` is polled and executes its function.\nThe function spawns a new thread to monitor the `nack-evt`, and then returns the actual event on which `sync` should try to synchronize.\nIn this case, we are returning a `channel-put-evt` that will *never* become ready because nothing is trying to read from the channel.\nAfter 2 seconds, this will print `nack-evt became ready!`.\n\n### Cancellation in the print service\n\nTo demonstrate a detailed example of nack events, we will make two minor improvements to our print service. When the\nprint service is handling a request but the user cancels it, we would like to handle this gracefully. There are\ntwo points at which we want to behave differently based on cancellation:\n\n1. If the operation is cancelled when submitting the request to the service, we can avoid submitting a request entirely.\n2. When the print service finishes the print and wants to respond, we want to respond only if the client is still listening. Previously, we solved this by using buffered channels. We can achieve the same result using nack events.\n\nHere is the Go version.\n\n```\n 1package main\n 2\n 3import (\n 4\t\"context\"\n 5\t\"fmt\"\n 6\t\"time\"\n 7)\n 8\n 9type PrintRequest struct {\n10\tmsg    string\n11\trespCh chan string\n12\tctx    context.Context\n13}\n14\n15type PrintService struct {\n16\tprintCh chan PrintRequest\n17}\n18\n19func MakePrintService() PrintService {\n20\tprintCh := make(chan PrintRequest)\n21\tgo func() {\n22\t\tvar req PrintRequest\n23\t\tfor {\n24\t\t\treq = <-printCh\n25\t\t\tfmt.Printf(\"Got print request: \\\"%s\\\"\\n\", req.msg)\n26\t\t\ttime.Sleep(1 * time.Second)\n27\t\t\tselect {\n28\t\t\tcase req.respCh <- \"success\":\n29\t\t\tcase <-req.ctx.Done():\n30\t\t\t\tfmt.Printf(\"Unable to respond to client who requested printing %s\\n\", req.msg)\n31\t\t\t}\n32\t\t}\n33\t}()\n34\treturn PrintService{\n35\t\tprintCh: printCh,\n36\t}\n37}\n38\n39func (ps *PrintService) Print(ctx context.Context, msg string) (string, error) {\n40\trespCh := make(chan string, 1)\n41\tselect {\n42\tcase ps.printCh <- PrintRequest{msg: msg, respCh: respCh, ctx: ctx}:\n43\tcase <-ctx.Done():\n44\t\treturn \"\", fmt.Errorf(\"Printer did not accept request %s!\", msg)\n45\t}\n46\tselect {\n47\tcase resp := <-respCh:\n48\t\treturn resp, nil\n49\tcase <-ctx.Done():\n50\t\treturn \"\", fmt.Errorf(\"Timeout waiting for response\")\n51\t}\n52}\n53\n54func main() {\n55\tservice := MakePrintService()\n56\n57\tfor i := range 2 {\n58\t\tctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)\n59\t\tdefer cancel()\n60\t\tresp, err := service.Print(ctx, fmt.Sprintf(\"Hello %d\", i))\n61\t\tif err != nil {\n62\t\t\tfmt.Printf(\"PrintService response error: %s\\n\", err)\n63\t\t} else {\n64\t\t\tfmt.Printf(\"PrintService: %s\\n\", resp)\n65\t\t}\n66\t}\n67\t// Wait a bit to see prints from the service\n68\ttime.Sleep(5 * time.Second)\n69}\n```\n\nNow Racket. First, let’s put the `nack-evt` in the `print-req` struct.\n\n```\n1(struct print-req (msg resp-ch abandoned-evt))\n```\n\n`print-evt` will send that to the service, and the service can [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29) on it while attempting to put the result in\n`resp-ch`.\n\n```\n 1(define (make-print-service)\n 2  (define (server-loop print-ch)\n 3    (let loop ()\n 4      (define req (channel-get print-ch))\n 5      (define abandoned-evt (print-req-abandoned-evt req))\n 6      (printf \"Got print request: ~a~n\" (print-req-msg req))\n 7      (sleep 1)\n 8      (sync (channel-put-evt (print-req-resp-ch req) 'success)\n 9            abandoned-evt)\n10      (loop)))\n11\n12  (define print-ch (make-channel))\n13  (thread (λ () (server-loop print-ch)))\n14  (print-service print-ch))\n```\n\nHere is the event construction. Rather than send the request to the service with a blocking put, we spawn a thread, and immediately return the response channel. When `print-evt` is used in a `sync`, if that `sync` selects another event, then `nack-evt` becomes ready. We never put the request into the print service channel.\n\n```\n1(define (print-evt service msg)\n2  (nack-guard-evt\n3   (λ (nack-evt)\n4     (define resp-ch (make-channel))\n5     (thread (λ ()\n6               (sync nack-evt\n7                     (channel-put-evt (print-service-print-ch service)\n8                                      (print-req msg resp-ch nack-evt)))))\n9     resp-ch)))\n```\n\nHere is the full code sample, with an intentionally short timeout and prints that show the nacks in action.\n\n```\n 1#lang racket/base\n 2\n 3(define (timeout-evt msec)\n 4  ; alarm-evt is roughly equivalent to WithDeadline. Translate a relative timeout to a deadline.\n 5  (alarm-evt (+ (current-inexact-monotonic-milliseconds) msec) #t))\n 6\n 7(struct print-service (print-ch))\n 8(struct print-req (msg resp-ch abandoned-evt))\n 9\n10(define (make-print-service)\n11  (define (server-loop print-ch)\n12    (let loop ()\n13      (define req (channel-get print-ch))\n14      (define abandoned-evt (print-req-abandoned-evt req))\n15      (printf \"Got print request: ~a~n\" (print-req-msg req))\n16      (sleep 1)\n17      (sync (channel-put-evt (print-req-resp-ch req) 'success)\n18            (handle-evt\n19             abandoned-evt\n20             (λ (_)\n21               (printf \"Unable to respond to client who requested printing ~v~n\"\n22                       (print-req-msg req)))))\n23      (loop)))\n24\n25  (define print-ch (make-channel))\n26  (thread (λ () (server-loop print-ch)))\n27  (print-service print-ch))\n28\n29(define (print-evt service msg)\n30  (nack-guard-evt\n31   (λ (nack-evt)\n32     (define resp-ch (make-channel))\n33     (thread (λ ()\n34               (sync (handle-evt nack-evt\n35                                 (λ (_) (printf \"Abandoning request ~v~n\" msg)))\n36                     (channel-put-evt (print-service-print-ch service)\n37                                      (print-req msg resp-ch nack-evt)))))\n38     resp-ch)))\n39\n40(module+ main\n41  (define service (make-print-service))\n42  (for ([i (in-range 2)])\n43    (sync (handle-evt (print-evt service (format \"Hello ~a\" i))\n44                      (λ (resp) (printf \"Printer response: ~a~n\" resp)))\n45          (handle-evt\n46           (timeout-evt 200)\n47           (λ (_)\n48             (printf \"Printer did not respond within 200ms on attempt ~v!~n\"\n49                     i))))))\n```\n\n```\nGot print request: Hello 0\nPrinter did not respond within 200ms on attempt 0!\nPrinter did not respond within 200ms on attempt 1!\nAbandoning request \"Hello 1\"\nUnable to respond to client who requested printing \"Hello 0\"\n```\n\nThe first attempt to sync on the `print-evt` at line 42 gets abandoned on the response, while the second one never even submits the\nrequest.\n\nRevisiting this block from the printing thread\n\n```\n1(sync (channel-put-evt (print-req-resp-ch req) 'success)\n2      abandoned-evt)\n```\n\nOne may wonder: this synchronizes on a nack event and the channel put, but could neither be ready for a while? Would the print service be blocked from servicing requests in that case? This won’t happen. By construction, the two events are associated with each other. Either the client has lost interest in the `resp-ch`, in which case the `abandoned-evt` is ready, or it is in a [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29) where `resp-ch` is one of the contenders for selection. Given that this print thread is now able to offer a value to the `resp-ch`, the two sides can immediately rendezvous and proceed, as long as the system is not otherwise CPU starved.\n\n*Negative acknowledgement events are a really neat way to express cancellation, because they take the idea of events and cheap threads and apply it to the problem of cancellation, instead of introducing any new primitives.*\n\nI hope that this journey through the print service gave you a glimpse of why the CML model is powerful and expressive:\n\n1. We were able to use the event combinators to encapsulate event operations, *such that the `print-evt` API never really changed, even as we layered on new capabilities*.\n2. To handle cancellation, we didn’t have to deal with a new concept of `Context`, *nor did any other part of the program need to know that `print-evt` needed to do something on cancellation*.\n3. If the caller wanted to queue up multiple print jobs, they could easily do so, keep the events in a list, then wait for all of them to finish.\n\n## Wrapping up loose threads\n\nOne of the things we have done with abandon in our Racket code is spawning threads and not worrying about shutting them down. Several of these threads seem to block on actions that may *never* unblock. In Go, this is a major source of bugs. The blocked goroutine never makes progress, and leaks its memory as well as any values it keeps alive. See the [go1.26 release\nnotes](https://go.dev/doc/go1.26#goroutineleak-profiles) for an example. Such leaks often have no side effects except slowly\nincreasing memory usage.\n\nIf the intent of Go is to\nmake goroutines too cheap to meter (an idea CML shares), then it should make them easier to use without requiring programmers to always consider how best to shut them down so that they don’t leak.\n\nCML addresses this, which is why we have been so care-free. **If a thread is waiting only on a blocking operation on events and there are no other\nlive references to either the thread or the events involved in the selection, then the thread is eligible for garbage\ncollection and eventual termination.**\n\nThis is a key ability that makes the CML concurrency model easier to use. The *Concurrent Programming in ML* book specifically calls it out:\n\n> In CML, however, these blocked threads are not a problem because they will be automatically garbage collected. When a\n> thread is blocked waiting for a synchronous operation to be enabled, its state is only reachable through the data\n> structures associated with the operation (e.g., through the channel waiting queue, when the thread is sending a\n> message). If those data structures are not reachable from some active thread, then both they, and the blocked thread,\n> will be garbage collected. […] This style of speculatively spawning threads to send communication that might never\n> be accepted is an important CML programming technique.\n\nRacket [retains these semantics](https://docs.racket-lang.org/reference/threads.html#:~:text=A%20thread%20that%20has%20not%20terminated%20can%20be%20garbage%20collected), even being careful with\n[`thread-dead-evt`](https://docs.racket-lang.org/reference/threads.html#%28def._%28%28quote._~23~25kernel%29._thread-dead-evt%29%29).\n[Here](https://gist.github.com/nikhilm/126757fd5b37f6b02451c96c26dcf4c1) is a slight variant of the above program\ndemonstrating the Racket service thread getting garbage collected when the service is no longer referenced.\n\nNotably, there is an [issue](https://github.com/golang/go/issues/19702) from March 2017, proposing similar semantics for\ngoroutines. In Go 1.27, a [leak detector](https://go.dev/doc/go1.27#goroutineleak-profiles) was shipped that uses\nsimilar semantics. Neither the proposal nor the [paper](https://users-cs.au.dk/~amoeller/papers/golf/paper.pdf) mentions\nthat this capability is available in another ecosystem, suggesting that not many people are aware of it.\n\nRetrofitting CML’s event system onto Go is not realistic at all. However, *allowing goroutines to be collected seems something Go should seriously consider in later language versions*.\n\n## `close`ing thoughts\n\nThe central idea behind Concurrent ML is simple - represent communication operations as values independently of their execution. Its effects are far-reaching. The separation allows composing and encapsulating these operations in a way Go cannot. The same abstraction can model choice (Go’s [`select`](https://go.dev/ref/spec#Select_statements)) and cancellation. Choice can have reasonably strong guarantees that *only one operation makes progress*, which allows expressing many mutual-exclusion requirements. It can express setup and post-commitment actions in a uniform way. There is truly a lot of elegance in this idea.\n\nThread-aware GC then allows ergonomic use of the event mechanism, without worrying about leaks or needing to layer accidental complexity.\n\nThis makes CML an interesting alternative to Go’s channels-and-select approach, as well as the `async/await` minima[3](#fn:3) that lots of other languages have settled on.\n\nThe CML model is worth studying even if you never intend to use it in production. If you’re reaching for Go for your next side project, consider reaching for Racket instead!\n\n*Thanks to Michael Dresser for feedback and discussion.*\n\n## References\n\n1. [Go Channels are bad and you should feel bad](https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad/)—an old post outlining some of the pitfalls of Go channels, and suggesting, even back then, that goroutines and channels should opt-in to garbage collection.\n2. *Concurrent Programming in ML* by John H. Reppy—the canonical book on CML, which collects information from the various Concurrent ML papers and presents several sophisticated examples of applying it to real software. This is out of print and hard to get, but worth it. It lays out the CML philosophy in a coherent narrative instead of working through the various papers.\n3. Andy Wingo has travelled some of this path across 3 posts, but with fewer examples:\n   1. [concurrent ml versus go — wingolog](https://wingolog.org/archives/2016/09/20/concurrent-ml-versus-go \"concurrent ml versus go — wingolog\")\n   2. [is go an acceptable cml? — wingolog](https://wingolog.org/archives/2016/09/21/is-go-an-acceptable-cml \"is go an acceptable cml? — wingolog\")\n   3. [An incomplete history of language facilities for concurrency — wingolog](https://wingolog.org/archives/2016/10/12/an-incomplete-history-of-language-facilities-for-concurrency \"An incomplete history of language facilities for concurrency — wingolog\")\n4. [*Kill-Safe Synchronization Abstractions*](https://www-old.cs.utah.edu/plt/publications/pldi04-ff.pdf) by Matthew Flatt and Robert Bruce Findler—introduces the CML primitives in Racket, then adds some more Racket-specific magic to create concurrent abstractions that are not only atomic with respect to multiple threads but also remain atomic in the presence of thread termination.\n5. [*A Design Space Exploration of Async/Await*](https://arxiv.org/pdf/2608.20677) by Gray, Krishnamurthi and Crichton—a fantastic recent paper teasing out the various aspects of async/await implementations and giving names to each aspect.\n\n## Appendix A: Events beyond channels\n\nSince Go sticks to CSP’s original model [4](#fn:4), it allows selection only on channels.\nIn this essay, we have been using channels to highlight the improvements CML makes over Go, and describe the event model. However, it is important to remember that events *are a general purpose abstraction* over any communication operation, being able to describe:\n\n- Network socket operations\n- Arbitrary file descriptors\n- Several in-language APIs, such as semaphores, threads and subprocesses\n\nThe original CML code and the Racket standard library provide event-based mechanisms for all of these. File descriptors from third-party libraries can be opted into the system, as can be custom, polling-based mechanisms. Here is a simple example of watching for changes to a file to demonstrate the versatility:\n\n```\n1#lang racket/base\n2(sync\n3 (handle-evt\n4  (filesystem-change-evt \"<path to a file>\")\n5  (λ (_) (printf \"File changed!~n\"))))\n```\n\nThis ability is not just a convenience wrapper. The Go equivalent cannot *just* spawn a goroutine to do the operation and then respond back on a channel that then gets used in a [`select`](https://go.dev/ref/spec#Select_statements). For example, Racket provides [`write-bytes-avail-evt`](https://docs.racket-lang.org/reference/Byte_and_String_Output.html#%28def._%28%28quote._~23~25kernel%29._write-bytes-avail-evt%29%29), which attempts to write bytes to a port (stream/[`io.Writer`](https://pkg.go.dev/io#Writer) equivalent). It can be directly used in a [`sync`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29). It *guarantees* that:\n\n1. Either the event was selected, and at least some of the bytes were written to the port, or\n2. The event did not get selected, and so no bytes were written.\n\nAs far as I know, it is not possible to provide such an API in Go. With a separate goroutine, the *commit point* ends up becoming the channel operation to relay the result, and not the I/O operation itself.\n\nRust Futures are the closest to CML events in several ways. They use polling-based readiness checks, which are a prerequisite for the CML model. `tokio::select!` is a macro that presents semantics similar to Go. However, Rust Futures differ in other ways that prevent building the CML model on top of them.\n\n## Appendix B: A small aside on the maker function\n\nBoth [`guard-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._guard-evt%29%29) and [`nack-guard-evt`](https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._nack-guard-evt%29%29) have relied on *maker functions* being passed to them. In this explainer I’ve assumed that the maker function is *always* called when used in `sync`. This is not actually the case. If one of the other events passed to `sync` is already *ready for synchronization*, then, based on pseudo-randomness, there is a possibility that it will immediately get selected, and no other event will even be polled. In such cases, the maker function will *NOT* be called. This is generally fine, since it is identical to the operation never being attempted. In our print example, this would mean the print request is never sent.\n\n---\n\n1. It doesn’t strictly have to return an event, it can also return a value, but that value will be swallowed\n   and the returned event will be immediately ready, which is not very valuable. [↩︎](#fnref:1)\n2. Cancellation is one of the hardest parts of designing a good concurrency model. A good model is important, but it also often requires users to be aware of and take great care about their own usage of the model. Take a look at the references for an exploration of the design space. [↩︎](#fnref:2)\n3. I think async-await was a bad model for most languages, apart from Rust. You are welcome to reach out to me for further discussion or wait for a subsequent essay. I am extremely happy that Java followed in Go’s footsteps rather than C#, JavaScript and Python. [↩︎](#fnref:3)\n4. The original CSP from 1978 did not have named channels. They were introduced in 1985, and this is the model Go uses. [↩︎](#fnref:4)\n","body_html":"<h1 id=\"concurrentml-improving-on-go-s-concurrency-model\">ConcurrentML: Improving on Go&#39;s concurrency model</h1>\n<p>Posted on Oct 16, 2026</p>\n<ul><li><a href=\"#events-communication-operations-as-values\">events: communication operations as values</a><ul><li><a href=\"#wrapping-events\">Wrapping events</a></li><li><a href=\"#composing-choice\">Composing choice</a></li></ul></li><li><a href=\"#building-on-events\">Building on events</a><ul><li><a href=\"#guard-events\">Guard events</a></li><li><a href=\"#handling-cancellation\">Handling cancellation</a></li><li><a href=\"#cancellation-in-the-print-service\">Cancellation in the print service</a></li></ul></li><li><a href=\"#wrapping-up-loose-threads\">Wrapping up loose threads</a></li><li><a href=\"#closeing-thoughts\"><code>close</code>ing thoughts</a></li><li><a href=\"#references\">References</a></li><li><a href=\"#appendix-events-beyond-channels\">Appendix: Events beyond channels</a></li></ul>\n<p><strong>*LLM use disclosure</strong>: This post is primarily written by a human. However, I&#39;ve used LLMs for speech-to-text, basic\ngrammar correction, feedback about the writing and for adding inline API documentation links automatically.*</p>\n<p>The Go programming language was instrumental in bringing the Communicating Sequential Processes (CSP) concurrency model\ninto widespread use. In the CSP model, channels are usually <em>unbuffered</em> and act not just as a means of communication,\nbut also for synchronization. That is, sending or receiving blocks until the other side is ready, and then the value is moved from one thread to another atomically.</p>\n<p>In addition, the <a href=\"https://go.dev/ref/spec#Select_statements\" rel=\"nofollow ugc noopener\"><code>select</code></a> statement guarantees that out of all the channel\noperations in its clauses, only one is selected and the others remain unprocessed. This allows several\nmessaging patterns to be expressed succinctly.</p>\n<p>CSP is an <a href=\"https://web.archive.org/web/20260922190027/https://www.cs.cmu.edu/~crary/819-f09/Hoare78.pdf\" title=\"Hoare&#39;s original paper\" rel=\"nofollow ugc noopener\">old idea</a> (from the 1970s), and while Go sticks relatively closely to that model, other languages have taken it\nfurther. In my opinion, the most interesting one is Concurrent ML (CML). Introduced in the mid-1990s, CML still has much to teach modern language designers.</p>\n<p>In this post, I want to argue that Concurrent ML’s developments on top of CSP offer compelling advantages for implementing concurrent systems, fixing several of Go’s deficiencies.</p>\n<p><strong>CML’s key idea is to promote a communication operation from an executable statement to a <em>value</em> describing the operation. The execution of that description can be done separately.</strong> Don’t worry if this isn’t clear right away, this whole essay is an elaboration of that idea.</p>\n<p>This unlocks several powerful improvements over Go/CSP:</p>\n<ol><li>It unifies the concepts of choice (what <code>select</code> does) and cancellation into a unified, general abstraction.</li><li>This abstraction enables composition and encapsulation of concurrency, which is impossible in Go.</li></ol>\n<p>Separately, CML fixes one of Go’s biggest concurrency footguns – goroutines leak unless properly shutdown – via straightforward garbage collection.</p>\n<p>While the ideas come from CML, today <a href=\"https://racket-lang.org\" rel=\"nofollow ugc noopener\">Racket</a> is the programming language that best espouses them.\nRacket is actively developed, and the CML model is comprehensively supported and used within it. With that in mind, I will be demonstrating all examples in Racket and will interchangeably\nuse the terms CML or Racket based on context.</p>\n<p>Similarly, I will use the words <em>goroutine</em> and <em>thread</em> interchangeably. Their semantics are identical, at least for the\npurposes of this article.</p>\n<p>This article does not propose anything new. It is meant to introduce practitioners to a model that has generally\nremained in the academic community. This article assumes that you are reasonably familiar with Go\nsyntax and the behavior of its channels. It does not assume familiarity directly with Racket, but you should be able to\nfollow basic ideas about function calls and recursion in a Scheme-like language.</p>\n<p>To try out the Racket code snippets, <a href=\"https://download.racket-lang.org/\" rel=\"nofollow ugc noopener\">download Racket</a>, fire up the DrRacket editor, and paste the examples in the main window. Press the Run button to execute.</p>\n<h2 id=\"events-communication-operations-as-values\">events: communication operations as values</h2>\n<p>In Go, if <code>ch</code> is a channel, the following two operations are used to send or receive messages on the channel.</p>\n<pre><code>1// Don&#39;t run this, it will deadlock\n2ch := make(chan int)\n3ch &lt;- 5\n4x := &lt;-ch</code></pre>\n<p>These channel operation statements <a href=\"https://go.dev/tour/concurrency/2\" rel=\"nofollow ugc noopener\">take effect immediately when reached</a>.</p>\n<p>When used with the <a href=\"https://go.dev/ref/spec#Select_statements\" rel=\"nofollow ugc noopener\"><code>select</code></a> statement, these expressions can be combined in <code>case</code> clauses to express\npotential choices that the runtime can select between. When execution reaches a <a href=\"https://go.dev/ref/spec#Select_statements\" rel=\"nofollow ugc noopener\"><code>select</code></a> statement, all channel operations are\npolled to see which ones are ready to execute. One operation is picked from those that are ready. It is\nguaranteed that no other channel operation will be completed within this execution.</p>\n<pre><code>1select {\n2case ch1 &lt;- 5:\n3case y := &lt;- ch2:\n4default:\n5    // selected if none of the others are ready.\n6}</code></pre>\n<p>Within the select, channel operations act as <em>descriptions</em>, which the <code>select</code> statement uses to determine which channels to poll, and for what operation. That is, hitting the first case does not immediately block the entirety of the <code>select</code>. However, in Go, this descriptive form cannot be decoupled from the <a href=\"https://go.dev/ref/spec#Select_statements\" rel=\"nofollow ugc noopener\"><code>select</code></a> statement and stand on its own. As soon as you try that, it becomes the previous example, and the statement tries to execute immediately. This prohibits composition and encapsulation. A function cannot return a description which you can choose to <code>select</code> on later. Nor can you stash the description within a struct for carrying around.</p>\n<p>Channels in Racket start of the same way and provide direct analogs of the blocking send and receive operations.</p>\n<pre><code>1; ch := make(chan int)\n2(define ch (make-channel))\n3; ch &lt;- 5\n4(channel-put ch 5)\n5; x := &lt;-ch\n6(define x (channel-get ch))</code></pre>\n<p>However, they also offer a standalone way to describe the operation.</p>\n<pre><code>1; sort of like `case ch1 &lt;- 5`.\n2(channel-put-evt ch 5)\n3\n4; There is no channel-get-evt.\n5; Instead a channel itself can directly be used as a description of the ability\n6; to receive from it.</code></pre>\n<p><strong>This is an expression returning a value <em>NOT</em> a statement of action!</strong></p>\n<p><code>channel-put-event</code> <strong>returns a value, which is called an <em>event</strong></em> in CML. It does not immediately send the value 5 on the channel.</p>\n<p>The event can be executed later to actually apply its effect. Because it is a value, just like the number 5, we can do a lot of useful things with it, which are not possible in Go:</p>\n<ul><li>assign it to a variable,</li><li>return it from a function,</li><li>put it into a struct,</li><li>abstract the fact that we are performing a channel operation behind a regular function, and</li><li>wrap it with additional concurrency combinators to express patterns</li></ul>\n<p>In addition, events are more general than channel operations, being able to express I/O operations that are commonly exposed by “event loops” from other languages.</p>\n<p>The event’s actual effects are put into motion by the <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a> function</p>\n<pre><code>1(sync (channel-put-evt ch 5))</code></pre>\n<p>Using <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a> <em>attempts to synchronize</em> a set of events. Just like <code>select</code>, all events passed to it are polled. An event that is <em>ready</em> is selected, and <code>sync</code> returns the <em>synchronization result</em> of the selected event, which is something each event can define. For sending on channels, this result is the event itself. When receiving from a channel it is the value received from the channel.</p>\n<p>When multiple events are ready, one is chosen randomly, just like Go.</p>\n<p>For example, here is a Racket thread that calculates the sum of a list of numbers and responds on a channel.</p>\n<pre><code> 1#lang racket/base\n 2(define (sum items resp-ch)\n 3  (define total (apply + items))\n 4  (channel-put resp-ch total))\n 5\n 6(define ch (make-channel))\n 7(define the-numbers (list 7 2 8 -9 4 0))\n 8\n 9(thread (λ () (sum the-numbers ch)))\n10\n11(define total (sync ch))\n12(printf &quot;Sum of ~a is ~a&quot; the-numbers total)</code></pre>\n<p>Note that this separation of description and execution can easily be recombined. <a href=\"https://docs.racket-lang.org/reference/channel.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._channel-put%29%29\" rel=\"nofollow ugc noopener\"><code>channel-put</code></a> can be defined as:</p>\n<pre><code>1(define (channel-put ch val)\n2  (sync (channel-put-evt ch val)))</code></pre>\n<h3 id=\"wrapping-events\">Wrapping events</h3>\n<blockquote><p>Throughout these essay, we will be using lots of anonymous functions. In Go, they are created with <code>func</code>. In Racket, the <a href=\"https://docs.racket-lang.org/reference/lambda.html#%28form._%28%28lib._racket%2Fprivate%2Fbase..rkt%29._lambda%29%29\" rel=\"nofollow ugc noopener\"><code>lambda</code></a> form, or literally <a href=\"https://docs.racket-lang.org/reference/lambda.html#%28form._%28%28lib._racket%2Fprivate%2Fbase..rkt%29._lambda%29%29\" rel=\"nofollow ugc noopener\"><code>λ</code></a> is used. If you want to refresh your knowledge of these, see <a href=\"https://go.dev/doc/codewalk/functions/\" rel=\"nofollow ugc noopener\">this</a> for Go or <a href=\"https://docs.racket-lang.org/guide/syntax-overview.html#%28part._.Anonymous_.Functions_with_lambda%29\" rel=\"nofollow ugc noopener\">this</a> for Racket.</p></blockquote>\n<p>The <a href=\"https://go.dev/tour/concurrency/5\" rel=\"nofollow ugc noopener\">first</a> example of the <a href=\"https://go.dev/ref/spec#Select_statements\" rel=\"nofollow ugc noopener\"><code>select</code></a> statement in the Tour of Go uses a goroutine to compute Fibonacci numbers, and relays the response using a channel. It also uses an additional channel to tell the goroutine to quit. This is the Racket translation:</p>\n<pre><code> 1#lang racket/base\n 2(define (fibonacci out-ch quit-ch)\n 3  (let loop ([x 0]\n 4             [y 1])\n 5    (sync\n 6     (handle-evt (channel-put-evt out-ch x) (λ (_) (loop y (+ x y))))\n 7     (handle-evt quit-ch (λ (_) (displayln &quot;quit&quot;))))))\n 8\n 9(module+ main\n10  (define c (make-channel))\n11  (define quit-ch (make-channel))\n12  (thread\n13   (λ ()\n14     (for ([i (in-range 10)])\n15       (displayln (channel-get c)))\n16     (channel-put quit-ch #f)))\n17  (fibonacci c quit-ch))</code></pre>\n<p>There are some functional-language-specific forms like the use of a named\n<code>let</code> to recursively express the <code>for {}</code> infinite loop. <code>in-range</code> is Racket’s number iteration form, so the <code>for</code> loops 10 times, retreiving the first 10 Fibonacci numbers. Just like <a href=\"https://go.dev/ref/spec#Select_statements\" rel=\"nofollow ugc noopener\"><code>select</code></a> accepts several channel operations,\n<a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a> accepts\nmultiple events, and then blocks until at least one of them is ready for synchronization.</p>\n<p>This first example introduces a new function <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._handle-evt%29%29\" rel=\"nofollow ugc noopener\"><code>handle-evt</code></a>. This is an <em>event combinator</em> - a function that augments events, and itself return an event.</p>\n<p>In Go’s <a href=\"https://go.dev/ref/spec#Select_statements\" rel=\"nofollow ugc noopener\"><code>select</code></a>, a <code>case</code> clause can be followed by a series of statements, which are executed iff that clause was selected and the operation executed. You can think of the <a href=\"https://go.dev/ref/spec#Select_statements\" rel=\"nofollow ugc noopener\"><code>select</code></a> clauses as being <em>commit points</em> where a specific communication operation gets committed to, and others are abandoned.</p>\n<p>With this mental model, we can do things before a commit point (<em>setup</em>) or after a commit point (<em>post-commitment</em>).</p>\n<p>Once we start composing events, it no longer makes sense to associate setup and post-commitment actions with\nthe <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a>, otherwise, whenever you returned an event from a function, you would also need to return the\nsetup actions separately. Instead, we can attach such steps to the event itself. This is done through a wrapper\nfunction that takes an event and a function. Each wrapper augments the event in a specific way. These are also called <em>event combinators</em>.</p>\n<p>Our first wrapper is <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._handle-evt%29%29\" rel=\"nofollow ugc noopener\"><code>handle-evt</code></a>, which associates <em>post-commitment</em> actions with an event. It returns a new, wrapped event. When the wrapped event\ngets selected by a <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a> (i.e. it wins the race), the associated function is run with the synchronization result of the event. In the example above, we use these actions to decide whether to continue looping or quit.</p>\n<p>Note that these wrappers are taking advantage of the general nature of events to put many common actions within that framework. All wrappers are encapsulating their input events, augmenting them, and returning output events. Users of those events <em>cannot</em> and <em>need not</em> figure out what is hiding behind the event.</p>\n<p>As another example of encapsulation, the quit <em>pattern</em> can be pulled into a <code>quit-evt</code>. Nothing outside the implementation needs to know that the quit is actually implemented by channels.</p>\n<pre><code> 1#lang racket/base\n 2; returns a quit-evt, and a function to trigger the quit.\n 3(define (make-quit-evt)\n 4  (define my-quit-ch (make-channel))\n 5  ; our quit-evt in this case is just a channel, but nobody needs to know that.\n 6  ; values is how a function returns multiple values in Racket.\n 7  (values my-quit-ch (λ () (channel-put my-quit-ch #f))))\n 8\n 9(define (fibonacci out-ch quit-evt)\n10  (let loop ([x 0]\n11             [y 1])\n12    ; It can later be used here.\n13    (sync (handle-evt (channel-put-evt out-ch x) (λ (_) (loop y (+ x y))))\n14          quit-evt)))\n15\n16(module+ main\n17  (define c (make-channel))\n18  ; define-values is an &quot;unpacking&quot; operation for a function returning multiple values.\n19  ; Similar to how result, err := Func() is used in Go.\n20  (define-values ( quit-evt quit-fn) (make-quit-evt))\n21  (thread (λ ()\n22            (for ([i (in-range 10)])\n23              (displayln (channel-get c)))\n24            (quit-fn)))\n25  (fibonacci c quit-evt))</code></pre>\n<p>I hope it has become clear that treating channel operations as descriptive values has unlocked composition!</p>\n<h3 id=\"composing-choice\">Composing choice</h3>\n<p>The ability to return events means that Racket code often builds up a list of events from various sources and then calls <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a> on all of them. <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a> is actually performing a <em>choice</em> operation between those events, and Racket allows that concept to also be separated into a wrapper event.</p>\n<p>The combinator is <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._choice-evt%29%29\" rel=\"nofollow ugc noopener\"><code>choice-evt</code></a>.</p>\n<p>Here is a common pattern of “fetch data from 2 different sources, and accept whichever one returns the fastest”.\nWe can easily compose it with timeouts. There isn’t a <a href=\"https://pkg.go.dev/context#Context\" rel=\"nofollow ugc noopener\"><code>context.Context</code></a> in sight!</p>\n<pre><code> 1#lang racket/base\n 2\n 3; This timeout-evt is used throughout the rest of the essay.\n 4; It translates a relative time to absolute time and uses it with the built-in alarm-evt.\n 5(define (timeout-evt msec)\n 6  (alarm-evt (+ (current-inexact-monotonic-milliseconds) msec) #t))\n 7\n 8; APIs are made-up\n 9(define (http-get-evt url)\n10    ...)\n11\n12(define (fetch-from-fastest-url url1 url2)\n13  (choice-evt (http-get-evt url1)\n14              (http-get-evt url2)))\n15\n16; Perform 2 http requests, returning the result of whichever responded fastest, but also capped with a timeout.\n17(sync (fetch-from-fastest-url url1 url2)\n18      (timeout-evt 5))</code></pre>\n<p>This sort of composition is <em>impossible</em> to express in Go, because it is not possible to wrap base operations or choices over them into re-usable functions. Spawning goroutines that return channels <em>is not the same</em>. In Racket, the <em>actual fetch operation does not start before <code>sync</code> is called</em>!</p>\n<p>I again want to stress that this composition of concurrency is a <em>really big deal</em>. Library code can assemble local concerns like choosing between two events, but let callers decide when to execute the operations, and callers themselves can impose their own rules, like timeouts, with neither needing to accomodate the other.</p>\n<h2 id=\"building-on-events\">Building on events</h2>\n<p>For the rest of this post, we will use a single running example to see how CML builds on the basic primitive of events to represent various patterns. We will discover how this system lends itself to a slightly different style of writing programs. It is <em>almost</em> an inversion of the way Go programs are written:</p>\n<ol><li>In CML, it is common to spawn threads <em>within</em> library APIs. In contrast, idiomatic Go libraries generally recommends asking callers to spawn a goroutine if they want concurrency.</li><li>Cancellation is <em>built into the system</em> instead of layered on top with <a href=\"https://pkg.go.dev/context#Context\" rel=\"nofollow ugc noopener\"><code>context.Context</code></a>. <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a> is aware of the cancellation APIs and actively participates in them.</li></ol>\n<p>In both languages, it is common to model a service that mediates access to a singleton\nresource <a href=\"https://gobyexample.com/stateful-goroutines\" rel=\"nofollow ugc noopener\">using a dedicated goroutine</a>. Say a <em>print service</em>, powering a <a href=\"https://en.wikipedia.org/wiki/Printer_(computing)\" rel=\"nofollow ugc noopener\">hardware printer</a>. Channels are used to send requests to the goroutine,\nwhich loops indefinitely. If the service accepts the channel request, then it <em>will</em> eventually\nsend the appropriate commands to the machine. It can perform queuing, and handle paper running out and so on, while keeping track of pending jobs. Obviously, in our examples, we will just <code>print</code> out to stdout.</p>\n<p>We also want to abstract the fact that the service is running in another thread, or even that it communicates via\nchannels. Obviously, this is ridiculous in such a tiny example, but bear with me here. Here is the simplest translation of these requirements to Go:</p>\n<pre><code> 1package main\n 2\n 3import (\n 4    &quot;fmt&quot;\n 5    &quot;time&quot;\n 6)\n 7\n 8type PrintService struct {\n 9    printCh chan string\n10}\n11\n12func MakePrintService() PrintService {\n13    printCh := make(chan string)\n14    go func() {\n15        var req string\n16        for {\n17            req = &lt;-printCh\n18            fmt.Printf(&quot;Got print request: \\&quot;%s\\&quot;\\n&quot;, req)\n19            // Uncomment this to simulate a slow service.\n20            // time.Sleep(3 * time.Second)\n21        }\n22\n23    }()\n24    return PrintService{\n25        printCh: printCh,\n26    }\n27}\n28\n29func (ps *PrintService) Print(msg string) {\n30    ps.printCh &lt;- msg\n31}\n32\n33func main() {\n34    service := MakePrintService()\n35    for _ = range 2 {\n36        service.Print(&quot;Hello World&quot;)\n37        time.Sleep(100 * time.Millisecond)\n38    }\n39}</code></pre>\n<p>Racket <code>struct</code>ures are similar to Go, just with different syntax:</p>\n<pre><code>1; A struct is defined like this.\n2(struct person (name age))\n3\n4; Instances are constructed like this\n5(define nikhil (person &quot;Nikhil&quot; 2))\n6\n7; Fields are accessed using getters of the form &lt;struct name&gt;-&lt;field name&gt;\n8(printf &quot;Hello, ~a!&quot; (person-name nikhil))</code></pre>\n<p>and the service:</p>\n<pre><code> 1#lang racket/base\n 2\n 3(struct print-service (print-ch))\n 4\n 5(define (make-print-service)\n 6  (define (server-loop print-ch)\n 7    (let loop ()\n 8      (define req (channel-get print-ch))\n 9      (printf &quot;Got print request: ~a~n&quot; req)\n10      ; (sleep 3)\n11      (loop)))\n12\n13  (define print-ch (make-channel))\n14  (thread (λ () (server-loop print-ch)))\n15\n16  (print-service print-ch))\n17\n18(define (print-evt service msg)\n19  (channel-put-evt (print-service-print-ch service) msg))\n20\n21(module+ main\n22  (define service (make-print-service))\n23  (for ([i (in-range 2)])\n24    (sync (print-evt service &quot;Hello World&quot;))))</code></pre>\n<h3 id=\"guard-events\">Guard events</h3>\n<p>A new requirement comes in that we should show the user whether the print succeeded. We need a way for the service to\nrespond to each print request. In both languages, <a href=\"https://go.dev/doc/effective_go#chan_of_chan\" rel=\"nofollow ugc noopener\">we can make a new channel per request to relay the response</a>. The\nchannel is sent to the service along with the request. Here is the Go code, with relevant changes highlighted:</p>\n<pre><code> 1package main\n 2\n 3\n 4import (\n 5    &quot;fmt&quot;\n 6    &quot;time&quot;\n 7)\n 8\n 9type PrintRequest struct {\n10    msg    string\n11    respCh chan string\n12}\n13\n14type PrintService struct {\n15    printCh chan PrintRequest\n16}\n17\n18func MakePrintService() PrintService {\n19    printCh := make(chan PrintRequest)\n20    go func() {\n21        var req PrintRequest\n22        for {\n23            req = &lt;-printCh\n24            fmt.Printf(&quot;Got print request: \\&quot;%s\\&quot;\\n&quot;, req.msg)\n25            // Uncomment this to simulate a slow service.\n26            // time.Sleep(3 * time.Second)\n27            time.Sleep(500 * time.Millisecond)\n28            req.respCh &lt;- &quot;success&quot;\n29        }\n30\n31    }()\n32    return PrintService{\n33        printCh: printCh,\n34    }\n35}\n36\n37func (ps *PrintService) Print(msg string) string {\n38    respCh := make(chan string, 1)\n39    ps.printCh &lt;- PrintRequest{msg: msg, respCh: respCh}\n40    return &lt;-respCh\n41}\n42\n43func main() {\n44    service := MakePrintService()\n45\n46    for _ = range 2 {\n47        resp := service.Print(&quot;Hello World&quot;)\n48        fmt.Printf(&quot;PrintService response: %s\\n&quot;, resp)\n49    }\n50}</code></pre>\n<p>This <code>Print()</code> does three different things. First, it mints a new response channel. This is the <em>setup</em> phase. Then,\nit sends the request and awaits the response.</p>\n<p>Turning to Racket, how would we achieve this? Where <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._handle-evt%29%29\" rel=\"nofollow ugc noopener\"><code>handle-evt</code></a> was an event combinator to express <em>post-commitment</em> actions, there is a dual combinator <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._guard-evt%29%29\" rel=\"nofollow ugc noopener\"><code>guard-evt</code></a> for expressing <em>setup</em> actions.</p>\n<p><a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._guard-evt%29%29\" rel=\"nofollow ugc noopener\"><code>guard-evt</code></a> is an event <em>maker</em>. It is a function that accepts a function, stores it somewhere, and returns an event. We will call this returned event a <em>guard event</em> to distinguish it clearly. When the guard event is <em>actually used</em> in\na <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a>, it does something special: It invokes the stored function, and expects the function to return an\nevent<a href=\"#fn:1\">1</a>. Then, the guard event “replaces” itself with the returned event. This naturally expresses\nthe setup phase.</p>\n<pre><code> 1#lang racket/base\n 2\n 3(define my-evt\n 4  (guard-evt\n 5   (lambda ()\n 6     (println &quot;I will be printed only when used in a sync&quot;)\n 7     ; A special built-in event that is never ready for synchronization.\n 8     never-evt)))\n 9\n10(define my-evt2\n11  (guard-evt\n12   (lambda ()\n13     (println &quot;I will be printed only when used in a sync&quot;)\n14     (timeout-evt 200))))\n15\n16; no output yet.\n17\n18(sync my-evt my-evt2)\n19; Prints &quot;I will be printed only when used in a sync&quot; **twice**!</code></pre>\n<p>Coming back to our print-service, here is how we can use <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._guard-evt%29%29\" rel=\"nofollow ugc noopener\"><code>guard-evt</code></a> to create the response channel:</p>\n<pre><code> 1#lang racket/base\n 2(require racket/async-channel)\n 3\n 4(struct print-service (print-ch))\n 5(struct print-req (msg resp-ch))\n 6\n 7(define (make-print-service)\n 8  (define (server-loop print-ch)\n 9    (let loop ()\n10      (define req (channel-get print-ch))\n11      (printf &quot;Got print request: ~a~n&quot; (print-req-msg req))\n12      (sleep 0.5)\n13      (async-channel-put (print-req-resp-ch req) &#39;success)\n14      (loop)))\n15\n16  (define print-ch (make-channel))\n17  (thread (λ () (server-loop print-ch)))\n18  (print-service print-ch))\n19\n20(define (print-evt service msg)\n21  (guard-evt (λ ()\n22               (define resp-ch (make-async-channel 1))\n23               (channel-put (print-service-print-ch service)\n24                            (print-req msg resp-ch))\n25               resp-ch)))\n26\n27(module+ main\n28  (define service (make-print-service))\n29  (for ([i (in-range 2)])\n30    (sync (handle-evt (print-evt service &quot;Hello World&quot;)\n31                      (λ (resp) (printf &quot;Printer response: ~a~n&quot; resp))))))</code></pre>\n<p>We introduce the struct <code>print-req</code> that will hold the request details. <code>print-evt</code> returns a <em>guard</em> event obtained from <code>guard-evt</code>.\nWhen called as a function, the lambda passed to <code>guard-evt</code> is not immediately invoked. Once <code>sync</code> starts executing at line 30, the lambda is invoked, and the set of events the <code>sync</code> is monitoring now contains <code>resp-ch</code>.</p>\n<h3 id=\"handling-cancellation\">Handling cancellation</h3>\n<p>So far we have demonstrated:</p>\n<ol><li>How events are descriptions of actions, and their effects only execute when used in <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a>.</li><li>How the CML model uses various combinators to frame setup and post-commitment actions.</li></ol>\n<p>We will now see how these features are combined to tackle the problem of cancellation/abandonment. In\nconcurrent systems, we often want to cancel some action because doing so will save resources or avoid some side effect. <a href=\"#fn:2\">2</a></p>\n<p>Common reasons are:</p>\n<ol><li>We are doing an action that could take unbounded time and we want to abandon it on timeout, or</li><li>The user or client is no longer interested in the result of the action, possibly because <em>some</em> other action succeeded.</li></ol>\n<p>In Go, the cancellation mechanism is established by convention. Functions that perform cancellable actions accept a\n<a href=\"https://pkg.go.dev/context#Context\" rel=\"nofollow ugc noopener\"><code>context.Context</code></a>, and monitor its <a href=\"https://pkg.go.dev/context#Context.Done\" rel=\"nofollow ugc noopener\"><code>Done()</code></a> channel concurrently with other actions (via <a href=\"https://go.dev/ref/spec#Select_statements\" rel=\"nofollow ugc noopener\"><code>select</code></a>). On the caller side,\nhelpers like <a href=\"https://pkg.go.dev/context#WithTimeout\" rel=\"nofollow ugc noopener\"><code>WithTimeout</code></a> or <a href=\"https://pkg.go.dev/context#WithCancel\" rel=\"nofollow ugc noopener\"><code>WithCancel</code></a> are used to close the channel backing <a href=\"https://pkg.go.dev/context#Context.Done\" rel=\"nofollow ugc noopener\"><code>Done()</code></a>.</p>\n<p>CML sees the hammer of events, and finds nails everywhere! Cancellation is expressed through <em>negative acknowledgement events</em> (<em>nack</em> events). <em>Unlike Go, this system is baked into the runtime.</em></p>\n<p><em>The rest of this section may seem complicated or hard to follow, so it will help to remember that cancellation is the aspect in which CML is closest to Go.</em> In both, there is an event (<code>ctx.Done()</code> in Go) that must be monitored to detect cancellation. It is only a conventional difference in where and how the event is created. In Go, <code>Context</code> is created at the top-level, and threaded through every function. CML sticks to the compositional advantages of events and event-combinators. Cancellation is opted into by individual events, by using a special guard combinator, and callers are none-the-wiser.</p>\n<p>As a quick refresher, here is how cancellation is expressed in Go using <code>Context</code>. Each function in the call heirarchy can add its own cancellation conditions, with custom timeouts, or direct calls to <code>cancel()</code>.</p>\n<pre><code> 1func DoMoreStuff(ctx context.Context) {\n 2  ...\n 3}\n 4\n 5func DoStuff(ctx context.Context) {\n 6  ctx, cancel := context.WithTimeout(ctx, 1*time.Second)\n 7  defer cancel()  // Don&#39;t forget this.\n 8  select {\n 9        op1:\n10            // do things\n11        op2:\n12            // do other things\n13  }\n14  // Whichever op is not selected can cancel based on monitoring Done()\n15}</code></pre>\n<p>In Racket, <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._nack-guard-evt%29%29\" rel=\"nofollow ugc noopener\"><code>nack-guard-evt</code></a> is the combinator that manufactures a negative acknowledgement (henceforth, <em>nack</em>) event. True to its name, it is <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._guard-evt%29%29\" rel=\"nofollow ugc noopener\"><code>guard-evt</code></a> with a slight twist. The maker function receives a special event as the first argument, like so:</p>\n<pre><code>1#lang racket/base\n2(nack-guard-evt (lambda (nack-evt)\n3  ...))</code></pre>\n<p>Just like <code>guard-evt</code>, the maker function should return the actual event to be used in a <code>sync</code>. <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._nack-guard-evt%29%29\" rel=\"nofollow ugc noopener\"><code>nack-guard-evt</code></a> establishes a one-to-one association between the nack event and the event returned from the maker function. Note that there are 3 different events to keep track of here, so let’s name them:</p>\n<ol><li>The event returned by <code>nack-guard-event</code> itself, which we will call the <em>guard event</em>.</li><li>The <em>original event</em> that the maker function will return, which will be a channel or similar.</li><li>The <code>nack-evt</code> that was created by <code>nack-guard-evt</code> and passed to the maker function.</li></ol>\n<p>The guard event is acting as a proxy for the original event, so, when used in a <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a>, three things can happen:</p>\n<ol><li>The original event is <em>ready and gets picked by <code>sync</code>.</em> In this case, <code>nack-evt</code> will <em>never</em> become ready.</li><li>The original event <em>does not get picked</em> by <code>sync</code>. In this case, <code>nack-evt</code> <em>will</em> become ready.</li><li>The <code>sync</code> is interrupted, the thread is killed or something throws an exception. <code>nack-evt</code> becomes ready in this case too!</li></ol>\n<p>Now, something else in the program needs to monitor the <code>nack-evt</code>’s readiness. This is conventionally done by spawning a separate thread to <code>sync</code> on that and <em>the actual operation that will lead to original event becoming ready</em>.</p>\n<p>Here is a quick example to make things clear.</p>\n<pre><code>1#lang racket/base\n2(define ch (make-channel))\n3\n4(sync (nack-guard-evt (lambda (nack-evt)\n5                        (thread (lambda ()\n6                                  (sync nack-evt)\n7                                  (println &quot;nack-evt became ready!&quot;)))\n8                        (channel-put-evt ch &quot;hello&quot;)))\n9      (timeout-evt 2000))</code></pre>\n<p>We wrap a <code>channel-put-evt</code> with <code>nack-guard-evt</code>. That is synchronized along with a <code>timeout-evt</code>.\nWhen execution reaches the <code>sync</code> then <code>nack-guard-evt</code> is polled and executes its function.\nThe function spawns a new thread to monitor the <code>nack-evt</code>, and then returns the actual event on which <code>sync</code> should try to synchronize.\nIn this case, we are returning a <code>channel-put-evt</code> that will <em>never</em> become ready because nothing is trying to read from the channel.\nAfter 2 seconds, this will print <code>nack-evt became ready!</code>.</p>\n<h3 id=\"cancellation-in-the-print-service\">Cancellation in the print service</h3>\n<p>To demonstrate a detailed example of nack events, we will make two minor improvements to our print service. When the\nprint service is handling a request but the user cancels it, we would like to handle this gracefully. There are\ntwo points at which we want to behave differently based on cancellation:</p>\n<ol><li>If the operation is cancelled when submitting the request to the service, we can avoid submitting a request entirely.</li><li>When the print service finishes the print and wants to respond, we want to respond only if the client is still listening. Previously, we solved this by using buffered channels. We can achieve the same result using nack events.</li></ol>\n<p>Here is the Go version.</p>\n<pre><code> 1package main\n 2\n 3import (\n 4    &quot;context&quot;\n 5    &quot;fmt&quot;\n 6    &quot;time&quot;\n 7)\n 8\n 9type PrintRequest struct {\n10    msg    string\n11    respCh chan string\n12    ctx    context.Context\n13}\n14\n15type PrintService struct {\n16    printCh chan PrintRequest\n17}\n18\n19func MakePrintService() PrintService {\n20    printCh := make(chan PrintRequest)\n21    go func() {\n22        var req PrintRequest\n23        for {\n24            req = &lt;-printCh\n25            fmt.Printf(&quot;Got print request: \\&quot;%s\\&quot;\\n&quot;, req.msg)\n26            time.Sleep(1 * time.Second)\n27            select {\n28            case req.respCh &lt;- &quot;success&quot;:\n29            case &lt;-req.ctx.Done():\n30                fmt.Printf(&quot;Unable to respond to client who requested printing %s\\n&quot;, req.msg)\n31            }\n32        }\n33    }()\n34    return PrintService{\n35        printCh: printCh,\n36    }\n37}\n38\n39func (ps *PrintService) Print(ctx context.Context, msg string) (string, error) {\n40    respCh := make(chan string, 1)\n41    select {\n42    case ps.printCh &lt;- PrintRequest{msg: msg, respCh: respCh, ctx: ctx}:\n43    case &lt;-ctx.Done():\n44        return &quot;&quot;, fmt.Errorf(&quot;Printer did not accept request %s!&quot;, msg)\n45    }\n46    select {\n47    case resp := &lt;-respCh:\n48        return resp, nil\n49    case &lt;-ctx.Done():\n50        return &quot;&quot;, fmt.Errorf(&quot;Timeout waiting for response&quot;)\n51    }\n52}\n53\n54func main() {\n55    service := MakePrintService()\n56\n57    for i := range 2 {\n58        ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)\n59        defer cancel()\n60        resp, err := service.Print(ctx, fmt.Sprintf(&quot;Hello %d&quot;, i))\n61        if err != nil {\n62            fmt.Printf(&quot;PrintService response error: %s\\n&quot;, err)\n63        } else {\n64            fmt.Printf(&quot;PrintService: %s\\n&quot;, resp)\n65        }\n66    }\n67    // Wait a bit to see prints from the service\n68    time.Sleep(5 * time.Second)\n69}</code></pre>\n<p>Now Racket. First, let’s put the <code>nack-evt</code> in the <code>print-req</code> struct.</p>\n<pre><code>1(struct print-req (msg resp-ch abandoned-evt))</code></pre>\n<p><code>print-evt</code> will send that to the service, and the service can <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a> on it while attempting to put the result in\n<code>resp-ch</code>.</p>\n<pre><code> 1(define (make-print-service)\n 2  (define (server-loop print-ch)\n 3    (let loop ()\n 4      (define req (channel-get print-ch))\n 5      (define abandoned-evt (print-req-abandoned-evt req))\n 6      (printf &quot;Got print request: ~a~n&quot; (print-req-msg req))\n 7      (sleep 1)\n 8      (sync (channel-put-evt (print-req-resp-ch req) &#39;success)\n 9            abandoned-evt)\n10      (loop)))\n11\n12  (define print-ch (make-channel))\n13  (thread (λ () (server-loop print-ch)))\n14  (print-service print-ch))</code></pre>\n<p>Here is the event construction. Rather than send the request to the service with a blocking put, we spawn a thread, and immediately return the response channel. When <code>print-evt</code> is used in a <code>sync</code>, if that <code>sync</code> selects another event, then <code>nack-evt</code> becomes ready. We never put the request into the print service channel.</p>\n<pre><code>1(define (print-evt service msg)\n2  (nack-guard-evt\n3   (λ (nack-evt)\n4     (define resp-ch (make-channel))\n5     (thread (λ ()\n6               (sync nack-evt\n7                     (channel-put-evt (print-service-print-ch service)\n8                                      (print-req msg resp-ch nack-evt)))))\n9     resp-ch)))</code></pre>\n<p>Here is the full code sample, with an intentionally short timeout and prints that show the nacks in action.</p>\n<pre><code> 1#lang racket/base\n 2\n 3(define (timeout-evt msec)\n 4  ; alarm-evt is roughly equivalent to WithDeadline. Translate a relative timeout to a deadline.\n 5  (alarm-evt (+ (current-inexact-monotonic-milliseconds) msec) #t))\n 6\n 7(struct print-service (print-ch))\n 8(struct print-req (msg resp-ch abandoned-evt))\n 9\n10(define (make-print-service)\n11  (define (server-loop print-ch)\n12    (let loop ()\n13      (define req (channel-get print-ch))\n14      (define abandoned-evt (print-req-abandoned-evt req))\n15      (printf &quot;Got print request: ~a~n&quot; (print-req-msg req))\n16      (sleep 1)\n17      (sync (channel-put-evt (print-req-resp-ch req) &#39;success)\n18            (handle-evt\n19             abandoned-evt\n20             (λ (_)\n21               (printf &quot;Unable to respond to client who requested printing ~v~n&quot;\n22                       (print-req-msg req)))))\n23      (loop)))\n24\n25  (define print-ch (make-channel))\n26  (thread (λ () (server-loop print-ch)))\n27  (print-service print-ch))\n28\n29(define (print-evt service msg)\n30  (nack-guard-evt\n31   (λ (nack-evt)\n32     (define resp-ch (make-channel))\n33     (thread (λ ()\n34               (sync (handle-evt nack-evt\n35                                 (λ (_) (printf &quot;Abandoning request ~v~n&quot; msg)))\n36                     (channel-put-evt (print-service-print-ch service)\n37                                      (print-req msg resp-ch nack-evt)))))\n38     resp-ch)))\n39\n40(module+ main\n41  (define service (make-print-service))\n42  (for ([i (in-range 2)])\n43    (sync (handle-evt (print-evt service (format &quot;Hello ~a&quot; i))\n44                      (λ (resp) (printf &quot;Printer response: ~a~n&quot; resp)))\n45          (handle-evt\n46           (timeout-evt 200)\n47           (λ (_)\n48             (printf &quot;Printer did not respond within 200ms on attempt ~v!~n&quot;\n49                     i))))))</code></pre>\n<pre><code>Got print request: Hello 0\nPrinter did not respond within 200ms on attempt 0!\nPrinter did not respond within 200ms on attempt 1!\nAbandoning request &quot;Hello 1&quot;\nUnable to respond to client who requested printing &quot;Hello 0&quot;</code></pre>\n<p>The first attempt to sync on the <code>print-evt</code> at line 42 gets abandoned on the response, while the second one never even submits the\nrequest.</p>\n<p>Revisiting this block from the printing thread</p>\n<pre><code>1(sync (channel-put-evt (print-req-resp-ch req) &#39;success)\n2      abandoned-evt)</code></pre>\n<p>One may wonder: this synchronizes on a nack event and the channel put, but could neither be ready for a while? Would the print service be blocked from servicing requests in that case? This won’t happen. By construction, the two events are associated with each other. Either the client has lost interest in the <code>resp-ch</code>, in which case the <code>abandoned-evt</code> is ready, or it is in a <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a> where <code>resp-ch</code> is one of the contenders for selection. Given that this print thread is now able to offer a value to the <code>resp-ch</code>, the two sides can immediately rendezvous and proceed, as long as the system is not otherwise CPU starved.</p>\n<p><em>Negative acknowledgement events are a really neat way to express cancellation, because they take the idea of events and cheap threads and apply it to the problem of cancellation, instead of introducing any new primitives.</em></p>\n<p>I hope that this journey through the print service gave you a glimpse of why the CML model is powerful and expressive:</p>\n<ol><li>We were able to use the event combinators to encapsulate event operations, <em>such that the <code>print-evt</code> API never really changed, even as we layered on new capabilities</em>.</li><li>To handle cancellation, we didn’t have to deal with a new concept of <code>Context</code>, <em>nor did any other part of the program need to know that <code>print-evt</code> needed to do something on cancellation</em>.</li><li>If the caller wanted to queue up multiple print jobs, they could easily do so, keep the events in a list, then wait for all of them to finish.</li></ol>\n<h2 id=\"wrapping-up-loose-threads\">Wrapping up loose threads</h2>\n<p>One of the things we have done with abandon in our Racket code is spawning threads and not worrying about shutting them down. Several of these threads seem to block on actions that may <em>never</em> unblock. In Go, this is a major source of bugs. The blocked goroutine never makes progress, and leaks its memory as well as any values it keeps alive. See the <a href=\"https://go.dev/doc/go1.26#goroutineleak-profiles\" rel=\"nofollow ugc noopener\">go1.26 release\nnotes</a> for an example. Such leaks often have no side effects except slowly\nincreasing memory usage.</p>\n<p>If the intent of Go is to\nmake goroutines too cheap to meter (an idea CML shares), then it should make them easier to use without requiring programmers to always consider how best to shut them down so that they don’t leak.</p>\n<p>CML addresses this, which is why we have been so care-free. <strong>If a thread is waiting only on a blocking operation on events and there are no other\nlive references to either the thread or the events involved in the selection, then the thread is eligible for garbage\ncollection and eventual termination.</strong></p>\n<p>This is a key ability that makes the CML concurrency model easier to use. The <em>Concurrent Programming in ML</em> book specifically calls it out:</p>\n<blockquote><p>In CML, however, these blocked threads are not a problem because they will be automatically garbage collected. When a\nthread is blocked waiting for a synchronous operation to be enabled, its state is only reachable through the data\nstructures associated with the operation (e.g., through the channel waiting queue, when the thread is sending a\nmessage). If those data structures are not reachable from some active thread, then both they, and the blocked thread,\nwill be garbage collected. […] This style of speculatively spawning threads to send communication that might never\nbe accepted is an important CML programming technique.</p></blockquote>\n<p>Racket <a href=\"https://docs.racket-lang.org/reference/threads.html#:~:text=A%20thread%20that%20has%20not%20terminated%20can%20be%20garbage%20collected\" rel=\"nofollow ugc noopener\">retains these semantics</a>, even being careful with\n<a href=\"https://docs.racket-lang.org/reference/threads.html#%28def._%28%28quote._~23~25kernel%29._thread-dead-evt%29%29\" rel=\"nofollow ugc noopener\"><code>thread-dead-evt</code></a>.\n<a href=\"https://gist.github.com/nikhilm/126757fd5b37f6b02451c96c26dcf4c1\" rel=\"nofollow ugc noopener\">Here</a> is a slight variant of the above program\ndemonstrating the Racket service thread getting garbage collected when the service is no longer referenced.</p>\n<p>Notably, there is an <a href=\"https://github.com/golang/go/issues/19702\" rel=\"nofollow ugc noopener\">issue</a> from March 2017, proposing similar semantics for\ngoroutines. In Go 1.27, a <a href=\"https://go.dev/doc/go1.27#goroutineleak-profiles\" rel=\"nofollow ugc noopener\">leak detector</a> was shipped that uses\nsimilar semantics. Neither the proposal nor the <a href=\"https://users-cs.au.dk/~amoeller/papers/golf/paper.pdf\" rel=\"nofollow ugc noopener\">paper</a> mentions\nthat this capability is available in another ecosystem, suggesting that not many people are aware of it.</p>\n<p>Retrofitting CML’s event system onto Go is not realistic at all. However, <em>allowing goroutines to be collected seems something Go should seriously consider in later language versions</em>.</p>\n<h2 id=\"closeing-thoughts\"><code>close</code>ing thoughts</h2>\n<p>The central idea behind Concurrent ML is simple - represent communication operations as values independently of their execution. Its effects are far-reaching. The separation allows composing and encapsulating these operations in a way Go cannot. The same abstraction can model choice (Go’s <a href=\"https://go.dev/ref/spec#Select_statements\" rel=\"nofollow ugc noopener\"><code>select</code></a>) and cancellation. Choice can have reasonably strong guarantees that <em>only one operation makes progress</em>, which allows expressing many mutual-exclusion requirements. It can express setup and post-commitment actions in a uniform way. There is truly a lot of elegance in this idea.</p>\n<p>Thread-aware GC then allows ergonomic use of the event mechanism, without worrying about leaks or needing to layer accidental complexity.</p>\n<p>This makes CML an interesting alternative to Go’s channels-and-select approach, as well as the <code>async/await</code> minima<a href=\"#fn:3\">3</a> that lots of other languages have settled on.</p>\n<p>The CML model is worth studying even if you never intend to use it in production. If you’re reaching for Go for your next side project, consider reaching for Racket instead!</p>\n<p><em>Thanks to Michael Dresser for feedback and discussion.</em></p>\n<h2 id=\"references\">References</h2>\n<ol><li><a href=\"https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad/\" rel=\"nofollow ugc noopener\">Go Channels are bad and you should feel bad</a>—an old post outlining some of the pitfalls of Go channels, and suggesting, even back then, that goroutines and channels should opt-in to garbage collection.</li><li><em>Concurrent Programming in ML</em> by John H. Reppy—the canonical book on CML, which collects information from the various Concurrent ML papers and presents several sophisticated examples of applying it to real software. This is out of print and hard to get, but worth it. It lays out the CML philosophy in a coherent narrative instead of working through the various papers.</li><li>Andy Wingo has travelled some of this path across 3 posts, but with fewer examples:<ol><li><a href=\"https://wingolog.org/archives/2016/09/20/concurrent-ml-versus-go\" title=\"concurrent ml versus go — wingolog\" rel=\"nofollow ugc noopener\">concurrent ml versus go — wingolog</a></li><li><a href=\"https://wingolog.org/archives/2016/09/21/is-go-an-acceptable-cml\" title=\"is go an acceptable cml? — wingolog\" rel=\"nofollow ugc noopener\">is go an acceptable cml? — wingolog</a></li><li><a href=\"https://wingolog.org/archives/2016/10/12/an-incomplete-history-of-language-facilities-for-concurrency\" title=\"An incomplete history of language facilities for concurrency — wingolog\" rel=\"nofollow ugc noopener\">An incomplete history of language facilities for concurrency — wingolog</a></li></ol></li><li><a href=\"https://www-old.cs.utah.edu/plt/publications/pldi04-ff.pdf\" rel=\"nofollow ugc noopener\"><em>Kill-Safe Synchronization Abstractions</em></a> by Matthew Flatt and Robert Bruce Findler—introduces the CML primitives in Racket, then adds some more Racket-specific magic to create concurrent abstractions that are not only atomic with respect to multiple threads but also remain atomic in the presence of thread termination.</li><li><a href=\"https://arxiv.org/pdf/2608.20677\" rel=\"nofollow ugc noopener\"><em>A Design Space Exploration of Async/Await</em></a> by Gray, Krishnamurthi and Crichton—a fantastic recent paper teasing out the various aspects of async/await implementations and giving names to each aspect.</li></ol>\n<h2 id=\"appendix-a-events-beyond-channels\">Appendix A: Events beyond channels</h2>\n<p>Since Go sticks to CSP’s original model <a href=\"#fn:4\">4</a>, it allows selection only on channels.\nIn this essay, we have been using channels to highlight the improvements CML makes over Go, and describe the event model. However, it is important to remember that events <em>are a general purpose abstraction</em> over any communication operation, being able to describe:</p>\n<ul><li>Network socket operations</li><li>Arbitrary file descriptors</li><li>Several in-language APIs, such as semaphores, threads and subprocesses</li></ul>\n<p>The original CML code and the Racket standard library provide event-based mechanisms for all of these. File descriptors from third-party libraries can be opted into the system, as can be custom, polling-based mechanisms. Here is a simple example of watching for changes to a file to demonstrate the versatility:</p>\n<pre><code>1#lang racket/base\n2(sync\n3 (handle-evt\n4  (filesystem-change-evt &quot;&lt;path to a file&gt;&quot;)\n5  (λ (_) (printf &quot;File changed!~n&quot;))))</code></pre>\n<p>This ability is not just a convenience wrapper. The Go equivalent cannot <em>just</em> spawn a goroutine to do the operation and then respond back on a channel that then gets used in a <a href=\"https://go.dev/ref/spec#Select_statements\" rel=\"nofollow ugc noopener\"><code>select</code></a>. For example, Racket provides <a href=\"https://docs.racket-lang.org/reference/Byte_and_String_Output.html#%28def._%28%28quote._~23~25kernel%29._write-bytes-avail-evt%29%29\" rel=\"nofollow ugc noopener\"><code>write-bytes-avail-evt</code></a>, which attempts to write bytes to a port (stream/<a href=\"https://pkg.go.dev/io#Writer\" rel=\"nofollow ugc noopener\"><code>io.Writer</code></a> equivalent). It can be directly used in a <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._sync%29%29\" rel=\"nofollow ugc noopener\"><code>sync</code></a>. It <em>guarantees</em> that:</p>\n<ol><li>Either the event was selected, and at least some of the bytes were written to the port, or</li><li>The event did not get selected, and so no bytes were written.</li></ol>\n<p>As far as I know, it is not possible to provide such an API in Go. With a separate goroutine, the <em>commit point</em> ends up becoming the channel operation to relay the result, and not the I/O operation itself.</p>\n<p>Rust Futures are the closest to CML events in several ways. They use polling-based readiness checks, which are a prerequisite for the CML model. <code>tokio::select!</code> is a macro that presents semantics similar to Go. However, Rust Futures differ in other ways that prevent building the CML model on top of them.</p>\n<h2 id=\"appendix-b-a-small-aside-on-the-maker-function\">Appendix B: A small aside on the maker function</h2>\n<p>Both <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28lib._racket%2Fprivate%2Fmisc..rkt%29._guard-evt%29%29\" rel=\"nofollow ugc noopener\"><code>guard-evt</code></a> and <a href=\"https://docs.racket-lang.org/reference/sync.html#%28def._%28%28quote._~23~25kernel%29._nack-guard-evt%29%29\" rel=\"nofollow ugc noopener\"><code>nack-guard-evt</code></a> have relied on <em>maker functions</em> being passed to them. In this explainer I’ve assumed that the maker function is <em>always</em> called when used in <code>sync</code>. This is not actually the case. If one of the other events passed to <code>sync</code> is already <em>ready for synchronization</em>, then, based on pseudo-randomness, there is a possibility that it will immediately get selected, and no other event will even be polled. In such cases, the maker function will <em>NOT</em> be called. This is generally fine, since it is identical to the operation never being attempted. In our print example, this would mean the print request is never sent.</p>\n<hr />\n<ol><li><p>It doesn’t strictly have to return an event, it can also return a value, but that value will be swallowed</p><p> and the returned event will be immediately ready, which is not very valuable. <a href=\"#fnref:1\">↩︎</a></p></li><li>Cancellation is one of the hardest parts of designing a good concurrency model. A good model is important, but it also often requires users to be aware of and take great care about their own usage of the model. Take a look at the references for an exploration of the design space. <a href=\"#fnref:2\">↩︎</a></li><li>I think async-await was a bad model for most languages, apart from Rust. You are welcome to reach out to me for further discussion or wait for a subsequent essay. I am extremely happy that Java followed in Go’s footsteps rather than C#, JavaScript and Python. <a href=\"#fnref:3\">↩︎</a></li><li>The original CSP from 1978 did not have named channels. They were introduced in 1985, and this is the model Go uses. <a href=\"#fnref:4\">↩︎</a></li></ol>","headings":[{"level":1,"text":"ConcurrentML: Improving on Go's concurrency model","id":"concurrentml-improving-on-go-s-concurrency-model"},{"level":2,"text":"events: communication operations as values","id":"events-communication-operations-as-values"},{"level":3,"text":"Wrapping events","id":"wrapping-events"},{"level":3,"text":"Composing choice","id":"composing-choice"},{"level":2,"text":"Building on events","id":"building-on-events"},{"level":3,"text":"Guard events","id":"guard-events"},{"level":3,"text":"Handling cancellation","id":"handling-cancellation"},{"level":3,"text":"Cancellation in the print service","id":"cancellation-in-the-print-service"},{"level":2,"text":"Wrapping up loose threads","id":"wrapping-up-loose-threads"},{"level":2,"text":"closeing thoughts","id":"closeing-thoughts"},{"level":2,"text":"References","id":"references"},{"level":2,"text":"Appendix A: Events beyond channels","id":"appendix-a-events-beyond-channels"},{"level":2,"text":"Appendix B: A small aside on the maker function","id":"appendix-b-a-small-aside-on-the-maker-function"}]}}