M-vars:   Synchronized Boxes
1 Guarantees
1.1 Ordering and Fairness
1.2 Atomicity
1.3 Thread Safety
2 Core Operations
make-mvar
mvar?
mvar-put!
mvar-try-put!
mvar-put!-evt
mvar-take!
mvar-try-take!
mvar-take!-evt
mvar-peek
mvar-try-peek
mvar-peek-evt
mvar-empty?
mvar-empty-evt
3 Derived Operations
mvar-swap!
mvar-update!
call-with-mvar
call-with-mvar!
4 Contracts
mvar/  c
5 Chaperones and Impersonators
impersonate-mvar
chaperone-mvar
6 Comparison with syncvar
Bibliography
8.12

M-vars: Synchronized Boxes🔗ℹ

Alexis King <lexi.lambda@gmail.com>

The source of this manual is available on GitHub.

 (require data/mvar) package: mvar-lib

M-vars originate in Haskell, where they are known as MVars. This library is based on the modern API provided by GHC, which differs in some details from their original presentation in Peyton Jones et al. (1996). Most notably, mvar-put! on a full M-var blocks (instead of raising an exception), and mvar-peek is atomic.

An M-var is a mutable data structure useful in concurrent programs. Like a box, an M-var is a mutable reference cell that can hold a single value. Unlike a box, an M-var can also be empty, holding no value at all. When a value is placed into an empty M-var using mvar-put!, the M-var becomes full, and it remains full until the value is removed using mvar-take!. If a thread attempts to put a value into an M-var that is already full, the thread waits until the M-var is emptied. Conversely, if a thread attempts to take a value from an M-var that is currently empty, it waits until the M-var is filled.
It is also possible to atomically read the contents of a full M-var without removing its value using mvar-peek. Like mvar-take!, using mvar-peek on an empty M-var waits until it is filled. Each operation also comes in a polling variant: mvar-try-put!, mvar-try-take!, and mvar-try-peek always return immediately and simply fail instead of blocking. For maximum flexibility, M-vars can also be combined with other synchronizable events using mvar-put!-evt, mvar-take!-evt, and mvar-peek-evt.

The blocking behavior of the M-var operations makes M-vars a remarkably flexible building block in concurrent programs, as they are effectively a box, semaphore, and channel rolled into one. Even a single M-var can serve numerous functions:
This list is far from exhaustive, and multiple M-vars used in concert can be even more flexible.

1 Guarantees🔗ℹ

1.1 Ordering and Fairness🔗ℹ

M-var synchronization is fair: if a thread is blocked on an M-var operation, and opportunities for the operation to complete occur infinitely often, the operation is guaranteed to eventually complete. However, the precise order in which threads blocked on a call to mvar-put! or mvar-take! are woken up is not guaranteed.

If a thread is blocked on a call to mvar-peek, the call is guaranteed to complete the next time the M-var is filled, even if another thread is blocked on a call to mvar-take! on the same M-var. In other words, whenever mvar-peek and mvar-take! compete to read the next value of an empty M-var, mvar-peek always wins. Since mvar-peek is not exclusive—that is, it does not preclude another thread from reading the same M-var after it completes—this preference for mvar-peek ensures that the maximum number of threads are woken up each time an M-var is filled.

1.2 Atomicity🔗ℹ

All M-var core operations are atomic: so long as their executing thread is not killed or suspended (see Thread Safety), either none or all of their effects will take place. This atomicity is guaranteed regardless of the way in which the M-var is used.

In contrast, derived operations internally perform multiple core operations in sequence, and those sequences are not intrinsically atomic. However, all derived operations currently provided by this library offer a weaker form of atomicity: they rely on the common M-var usage pattern of treating the M-var like a semaphore-protected box, where each use of mvar-put! is preceded by a use of mvar-take! to acquire exclusive access. This exclusive locking discipline ensures that derived operations are atomic with respect to the M-var’s state—each operation constitutes a non-overlapping “transaction” that is always fully committed or fully rolled back—but following the pattern correctly is the programmer’s responsibility.

1.3 Thread Safety🔗ℹ

M-vars are a concurrent data structure, so all M-var operations are naturally thread-safe. Furthermore, they are also break-safe: if a thread is interrupted via break-thread while executing any M-var operation, its atomicity guarantees will not be compromised, and the M-var will always remain in a valid state.

However, M-vars are not kill-safe. If a thread is killed via kill-thread while executing any M-var operation (including mvar-peek), the operation’s effects may only partially complete, and the M-var may be permanently left in an invalid state. In a similar vein, if a thread is suspended via thread-suspend while executing any M-var operation, the operation may only partially complete, and the M-var may become temporarily unusable until the thread is resumed.

M-var operations also cannot safely be called in atomic mode. Even non-blocking operations like mvar-try-peek may require polling events, which can lead to a deadlock if atomic mode is active.

2 Core Operations🔗ℹ

This section documents the complete list of M-var core operations. All core operations are primitive (they cannot be derived from other operations) and atomic (but not kill-safe; see Thread Safety).

procedure

(make-mvar)  mvar?

(make-mvar v)  mvar?
  v : any/c
Creates and returns a new M-var. If called with no arguments, the returned M-var is initially empty. If called with one argument, the returned M-var is initially full and contains v.

Examples:
> (make-mvar)

#<mvar: empty>

> (make-mvar 42)

#<mvar: 42>

procedure

(mvar? v)  boolean?

  v : any/c
Returns #t if v is an M-var, otherwise returns #f.

procedure

(mvar-put! mv    
  v    
  [#:enable-break? enable-break?])  void?
  mv : mvar?
  v : any/c
  enable-break? : any/c = #f
Fills mv with the value v. If mv is already full, mvar-put! blocks until it is emptied.

If enable-break? is not #f, breaks are explicitly enabled while waiting on mv. If breaks are disabled when mvar-put! is called, then either mv is filled or an exn:break exception is raised, but not both.

Examples:
> (define mv (make-mvar))
> mv

#<mvar: empty>

> (mvar-put! mv 42)
> mv

#<mvar: 42>

procedure

(mvar-try-put! mv v)  boolean?

  mv : mvar?
  v : any/c
If mv is currently empty, mvar-try-put! fills it and returns #t. Otherwise, returns #f.

Examples:
> (define mv (make-mvar))
> mv

#<mvar: empty>

> (mvar-try-put! mv 1)

#t

> mv

#<mvar: 1>

> (mvar-try-put! mv 2)

#f

> mv

#<mvar: 1>

procedure

(mvar-put!-evt mv v)  evt?

  mv : mvar?
  v : any/c
Returns a synchronizable event for use with sync. The event is ready for synchronization when mv is empty, and if the event is selected, mv is filled with v. The event’s synchronization result is the event itself.

procedure

(mvar-take! mv    
  [#:enable-break? enable-break?])  any/c
  mv : mvar?
  enable-break? : any/c = #f
Removes the value contained in mv and returns it. If mv is currently empty, mvar-take! blocks until it is filled.

If enable-break? is not #f, breaks are explicitly enabled while waiting on mv. If breaks are disabled when mvar-take! is called, then either mv is emptied or an exn:break exception is raised, but not both.

Examples:
> (define mv (make-mvar 42))
> mv

#<mvar: 42>

> (mvar-take! mv)

42

> mv

#<mvar: empty>

procedure

(mvar-try-take! mv [fail])  any

  mv : mvar?
  fail : failure-result/c = #f
If mv is currently full, mvar-try-take! removes its value and returns it. If mv is currently empty, fail determines the result:
  • If fail is a procedure, it is applied to zero arguments in tail position to produce the result.

  • Otherwise, fail is returned as the result.

Examples:
> (define mv (make-mvar 42))
> mv

#<mvar: 42>

> (mvar-try-take! mv)

42

> mv

#<mvar: empty>

> (mvar-try-take! mv)

#f

procedure

(mvar-take!-evt mv)  evt?

  mv : mvar?
Returns a synchronizable event for use with sync. The event is ready for synchronization when mv is full. If the event is selected, mv is emptied, and the removed value is the event’s synchronization result.

procedure

(mvar-peek mv    
  [#:enable-break? enable-break?])  any/c
  mv : mvar?
  enable-break? : any/c = #f
Returns the value contained in mv. If mv is currently empty, mvar-peek blocks until it is filled.

If enable-break? is not #f, breaks are explicitly enabled while waiting on mv. If breaks are disabled when mvar-peek is called, then either mv is emptied or an exn:break exception is raised, but not both.

Examples:
> (define mv (make-mvar 42))
> (mvar-peek mv)

42

> mv

#<mvar: 42>

> (mvar-peek mv)

42

Note that mvar-take! followed immediately by a use of mvar-put! to replace the taken value is not equivalent to mvar-peek: since mvar-take! empties the M-var, another thread may fill it with a different value before the removed value can be replaced. In comparison, mvar-peek does not remove the value from the M-var, so it is guaranteed to be atomic. Additionally, a call to mvar-peek is guaranteed to return as soon as the M-var is filled, while mvar-take! is not; see Ordering and Fairness.

procedure

(mvar-try-peek mv [fail])  any

  mv : mvar?
  fail : failure-result/c = #f
If mv is currently full, mvar-try-peek returns its value. If mv is currently empty, fail determines the result in the same was as for mvar-try-take!.

Examples:
> (define mv (make-mvar 42))
> (mvar-try-peek mv)

42

> (mvar-take! mv)

42

> (mvar-try-peek mv)

#f

procedure

(mvar-peek-evt mv)  evt?

  mv : mvar?
Returns a synchronizable event for use with sync. The event is ready for synchronization when mv is full, and its value is the event’s synchronization result.

procedure

(mvar-empty? mv)  boolean?

  mv : mvar?
Returns #t if mv is currently empty, otherwise returns #f.

This operation is provided for completeness, but note that if mv has multiple readers, the result of this function could become out of date the moment it returns. It is therefore very rarely the right choice, and it is almost always better to use mvar-try-put!, mvar-try-take!, or mvar-try-peek, instead.

procedure

(mvar-empty-evt mv)  evt?

  mv : mvar?
Returns a synchronizable event for use with sync. The event is ready for synchronization when mv is empty, and its synchronization result is the event itself.

Like mvar-empty?, this operation should be used very carefully: even if the event is selected, another thread might fill mv the instant that sync returns, so it is almost always better to use mvar-put!-evt, instead. However, in programs where mv only has a single writer, it can rarely be useful, so it is provided for completeness.

3 Derived Operations🔗ℹ

The bindings documented in this section are derived operations, which codify some common patterns that arise when an M-var is used like a semaphore-protected box. Since they are implemented as sequences of core operations, they are not intrinsically atomic. Instead, atomicity is enforced through a locking discipline: each operation expects to receive an M-var that is normally kept full, and it uses mvar-take! to acquire an exclusive lock on its contents. When the operation returns, it uses mvar-put! to simultaneously update the contents and release the lock.

Since all of the bindings in this section follow this locking discipline, an easy recipe to ensure atomicity is to never use mvar-take! or mvar-put! directly on any M-var used as a semaphore-protected box. Doing this comes at the risk of accidentally failing to either acquire or release the lock, which is especially easy to do if the critical section fails with an exception or is interrupted by a break. The higher-level, derived operations documented in this section make those accidents impossible, so they should be preferred whenever the lower-level, channel-style operations are not needed.

procedure

(mvar-swap! mv    
  v    
  [#:enable-break? enable-break?])  any/c
  mv : mvar?
  v : any/c
  enable-break? : any/c = #f
Takes a value from mv, puts v into mv, then returns the taken value. If enable-break? is not #f, breaks are explicitly enabled while waiting to take from mv.

Since mvar-swap! is implemented using mvar-take! followed by mvar-put!, it is not intrinsically atomic. To ensure atomicity, mvar-swap! should only be used on M-vars that follow the required locking discipline.

Examples:
> (define mv (make-mvar 'old))
> (mvar-swap! mv 'new)

'old

> mv

#<mvar: 'new>

procedure

(mvar-update! mv    
  update-proc    
  [#:enable-break? enable-break?])  void?
  mv : mvar?
  update-proc : (-> any/c any/c)
  enable-break? : any/c = #f
Takes a value from mv, applies update-proc to the taken value, then puts the result into mv. If enable-break? is not #f, breaks are explicitly enabled while waiting to take from mv.

Since mvar-update! is implemented using mvar-take! followed by mvar-put!, it is not intrinsically atomic. To ensure atomicity, mvar-update! should only be used on M-vars that follow the required locking discipline.

Examples:
> (define mv (make-mvar 0))
> (mvar-update! mv add1)
> mv

#<mvar: 1>

procedure

(call-with-mvar mv    
  body-proc    
  [#:enable-break? enable-break?])  any
  mv : mvar?
  body-proc : (-> any/c any)
  enable-break? : any/c = #f
Takes a value from mv, applies body-proc to the taken value, then puts the taken value back into mv. The result of body-proc is the result of the call-with-mvar call. If enable-break? is not #f, breaks are explicitly enabled while waiting to take from mv.

call-with-mvar is useful if mv contains a handle to a shared resource that cannot be used by more than one thread at a time. For example, mv might hold an output port, and a writer thread might use call-with-mvar to obtain exclusive access to the port while it writes a packet, which must not be interleaved with other writes. Once the packet has been written and body-proc returns, call-with-mvar puts the output port back in mv, making it available for acquisition by some other thread.

Since call-with-mvar is implemented using mvar-take! followed by mvar-put!, it is not intrinsically atomic. To ensure atomicity, call-with-mvar should only be used on M-vars that follow the required locking discipline.

procedure

(call-with-mvar! mv    
  body-proc    
  [#:enable-break? enable-break?])  any
  mv : mvar?
  body-proc : (-> any/c (values any/c ...+))
  enable-break? : any/c = #f
Takes a value from mv and applies body-proc to the taken value, which must return at least one result. The first result is put into mv, and the other results are the result of the call-with-mvar! call. If enable-break? is not #f, breaks are explicitly enabled while waiting to take from mv.

In other words, call-with-mvar! is effectively a combination of call-with-mvar and mvar-update!: body-proc determines both the value put back into mv and the result (or results) of the overall call.

Since call-with-mvar! is implemented using mvar-take! followed by mvar-put!, it is not intrinsically atomic. To ensure atomicity, call-with-mvar! should only be used on M-vars that follow the required locking discipline.

4 Contracts🔗ℹ

procedure

(mvar/c in-ctc [out-ctc])  contract?

  in-ctc : contract?
  out-ctc : contract? = in-ctc
Returns a contract that recognizes M-vars. Values written to the M-var must match in-ctc, and values read from the M-var must match out-ctc. Usually, in-ctc and out-ctc are the same (which is the default if out-ctc is not provided), but supplying none/c for one of the arguments can be useful to restrict the client of the contract to reading from or writing to the M-var.

If in-ctc and out-ctc are both chaperone contracts, the result will be a chaperone contract. Otherwise, the result will be an impersonator contract.

Examples:
> (define/contract mv (mvar/c exact-integer?) (make-mvar))
> (mvar-put! mv 'not-an-integer)

mv: contract violation

  expected: exact-integer?

  given: 'not-an-integer

  in: a value written to

      (mvar/c exact-integer?)

  contract from: (definition mv)

  blaming: top-level

   (assuming the contract is correct)

  at: eval:1:0

5 Chaperones and Impersonators🔗ℹ

procedure

(impersonate-mvar mv    
  [#:get get-proc    
  #:put put-proc]    
  prop    
  prop-val ...    
  ...)  mvar?
  mv : mvar?
  get-proc : (or/c (-> any/c any/c) #f) = #f
  put-proc : (or/c (-> any/c any/c) #f) = #f
  prop : impersonator-property?
  prop-val : any/c
Returns an impersonator of mv.

If get-proc is not #f, the result of each use of mvar-take! or mvar-peek on the impersonator is redirected through get-proc, which must produce a replacement value. Likewise, if put-proc is not #f, the value stored by each use of mvar-put! is redirected through put-proc.

Pairs of prop and prop-val (the number of by-position arguments to impersonate-mvar must be odd) add or override impersonator property values of mv.

procedure

(chaperone-mvar mv    
  [#:get get-proc    
  #:put put-proc]    
  prop    
  prop-val ...    
  ...)  mvar?
  mv : mvar?
  get-proc : (or/c (-> any/c any/c) #f) = #f
  put-proc : (or/c (-> any/c any/c) #f) = #f
  prop : impersonator-property?
  prop-val : any/c
Like impersonate-mvar, but produces a chaperone of mv, and the get-proc and put-proc procedures must return chaperones of their arguments.

6 Comparison with syncvar🔗ℹ

The syncvar/mvar library predates data/mvar and also provides an implementation of M-vars. The libraries are quite similar, but data/mvar provides several additional features:
For most users, the first two bullets in the above list will likely be the only ones that matter.

Bibliography🔗ℹ

Simon Peyton Jones, Andrew Gordon, and Sigbjorn Finne. Concurrent Haskell. Principles of Programming Languages (POPL), 1996. doi:10.1145/237721.237794