# Notes on Concurrency

**URL:** <https://racket.discourse.group/t/notes-on-concurrency/3794>\
**Category:** General\
**Created:** [June 16, 2025, 2:13am UTC](https://racket.discourse.group/t/notes-on-concurrency/3794 "2025-06-16T02:13:11Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![maueroats](https://avatars.discourse-cdn.com/v4/letter/m/c0e974/32.png) [@maueroats](https://racket.discourse.group/u/maueroats)\
**Post date:** [June 16, 2025, 2:13am UTC](https://racket.discourse.group/t/notes-on-concurrency/3794/1 "2025-06-16T02:13:11Z")

</div>

This post summarizes parts of a conversation that occurred on Discord. I am not the author of any of the commentary. I am posting it here because I liked the discussion and I don't want it lost in unsearchable Discord history.

## Patterns of Composition

I've thought about this a lot over the years and the conclusion I reached is that there's two patterns of composition for two concurrent operations `a` and `b` :

- exclusive choice, where exactly one of `a` or `b` happens and it's whichever is ready to happen first

- product, where both happen and if one fails the other is cancelled and synchronizable events with non-cooperative threads work perfectly for the former but the latter is better served by cooperative concurrency.

### Discussion

1. i always thought that the advantage of synchronizable events is that they graceful handle cancellation in a way that some other concurrency systems dont; i remember playing with channels in ocaml's `eio` and getting into situations where "racing" two channels picks one but the cancellation request to the other fails to prevent it from consuming a message
2. the problem is that there's no way to combine two events into a compound event that preserves the exclusive-or guarantee of `sync`
3. isn't that what `choice-evt` is? but it requires your operations to be events all the way down which can be difficult to do
4. by "compound event" I mean combining two events into a single event where syncing on it syncs on _both_ input events, not _exactly one_ of them, so like `(zip-event f e1 e2)` applicative functor composition
5. sync events are bad at doing this?
6. yes. they cannot do it correctly.

### Example

Suppose there are lock objects that count as synchronizable events, and syncing on them acquires the lock. then consider this operation:

```scheme
(sync (zip-evt list lock1 lock2)
      lock3)

```

It is possible that this code acquires `lock1` , then acquires `lock3` and selects the `lock3` events as the result of `sync` .... and the "negative acknowledgment" event for `lock1` never fires. So the code could silently acquire `lock1` without realizing it.

The issue is that _exactly one_ of these two things happens:

- `lock1` gets acquired
- the `nack` for `lock1` becomes ready

Just because `lock1` is ready, doesn't mean `lock2` is ready. there's no way to tell `lock1` to "wait" until `lock2` is also ready. the only thing `zip-evt` can do is try to sync on both, then use its own nack to manually _undo_ the lock acquisition if one lock gets acquired but the other doesn't. this isn't particularly composable, since the event interface doesn't expose a general way to "undo" an event that's already happened. It only guarantees that only one event can happen at once.

There's a variant of concurrent ML that lets you make events with transactions and rollback semantics, but it's much more complex and not as fast.

### Misc

Suggestion: see [a join calculus tutorial](https://www.microsoft.com/en-us/research/wp-content/uploads/2017/01/join-tutorial.pdf) for exploration of the space.

## Conclusion

Question: what does that mean for a casual `sync` user?

Answer: it means you shouldn't try to make your own custom events which try to wait on multiple sub-events to all complete. and it means that you should be wary of `replace-evt`. If you want to wait on multiple things to happen asynchronously, you should probably use `delay/thread` for each subtask, then just `force` the subtasks serially. and you should give up on optimal cancellation.

---

<div class="post-metadata">

**Author:** ![maueroats](https://avatars.discourse-cdn.com/v4/letter/m/c0e974/32.png) [@maueroats](https://racket.discourse.group/u/maueroats)\
**Post date:** [June 16, 2025, 2:15am UTC](https://racket.discourse.group/t/notes-on-concurrency/3794/2 "2025-06-16T02:15:35Z")

</div>

Further commentary, also not mine:

A `nack` event passed to the guard proc of a `nack-guard-evt` _is_ guaranteed to become ready for synchronization if the event returned by `nack-guard-evt` is abandoned. What's not guaranteed is that a composition of events will behave as you expect upon abandonment. For example:

```scheme
(define sema (make-semaphore 1))
(let loop ()
  (sync
   (replace-evt ;; 1
    sema
    (lambda (_)
      (handle-evt ;; 2
       (system-idle-evt)
       (lambda (_)
         (begin0 'a
           (semaphore-post sema))))))
   (wrap-evt always-evt (λ (_) 'b))) ;; 3
  (if (semaphore-try-wait? sema)
      (semaphore-post sema)
      (error "deadlock"))
  (loop))

```

This code can deadlock because, when 1 is selected for synchronization, the semaphore is decremented and the sync recurs with 2 and 3 (as opposed to 1 and 3 on the first iteration). When 3 is selected on that retry, sema remains decremented so the next iteration will deadlock.
