6.22.5 Mutexes and Condition Variables

Mutexes are low-level primitives used to coordinate concurrent access to mutable data. Short for “mutual exclusion”, the name “mutex” indicates that only one thread at a time can acquire access to data that is protected by a mutex – threads are excluded from accessing data at the same time. If one thread has locked a mutex, then another thread attempting to lock that same mutex will wait until the first thread is done.

Mutexes can be used to build robust multi-threaded programs that take advantage of multiple cores. However, they provide very low-level functionality and are somewhat dangerous; usually you end up wanting to acquire multiple mutexes at the same time to perform a multi-object access, but this can easily lead to deadlocks if the program is not carefully written. For example, if objects A and B are protected by associated mutexes M and N, respectively, then to access both of them then you need to acquire both mutexes. But what if one thread acquires M first and then N, at the same time that another thread acquires N them M? You can easily end up in a situation where one is waiting for the other.

There’s no easy way around this problem on the language level. A function A that uses mutexes does not necessarily compose nicely with a function B that uses mutexes. For this reason we suggest using atomic variables when you can (see Atomics), as they do not have this problem.

Still, if you as a programmer are responsible for a whole system, then you can use mutexes as a primitive to provide safe concurrent abstractions to your users. (For example, given all locks in a system, if you establish an order such that M is consistently acquired before N, you can avoid the “deadly-embrace” deadlock described above. The problem is enumerating all mutexes and establishing this order from a system perspective.) Guile gives you the low-level facilities to build such systems.

In Guile there are additional considerations beyond the usual ones in other programming languages: non-local control flow and asynchronous interrupts. What happens if you hold a mutex, but somehow you cause an exception to be thrown? There is no one right answer. You might want to keep the mutex locked to prevent any other code from ever entering that critical section again. Or, your critical section might be fine if you unlock the mutex “on the way out”, via an exception handler or dynamic-wind. See Exceptions, and See Dynamic Wind.

But if you arrange to unlock the mutex when leaving a dynamic extent via dynamic-wind, what to do if control re-enters that dynamic extent via a continuation invocation? Surely re-entering the dynamic extent without the lock is a bad idea, so there are two options on the table: either prevent re-entry via with-continuation-barrier or similar, or reacquire the lock in the entry thunk of a dynamic-wind.

You might think that because you don’t use continuations, that you don’t have to think about this, and you might be right. If you control the whole system, you can reason about continuation use globally. Or, if you know all code that can be called in a dynamic extent, and none of that code can call continuations, then you don’t have to worry about re-entry, and you might not have to worry about early exit either.

However, do consider the possibility of asynchronous interrupts (see Asynchronous Interrupts). If the user interrupts your code interactively, that can cause an exception; or your thread might be cancelled, which does the same; or the user could be running your code under some pre-emptive system that periodically causes lightweight task switching. (Guile does not currently include such a system, but it’s possible to implement as a library.) Probably you also want to defer asynchronous interrupt processing while you hold the mutex, and probably that also means that you should not hold the mutex for very long.

All of these additional Guile-specific considerations mean that from a system perspective, you would do well to avoid these hazards if you can by not requiring mutexes. Instead, work with immutable data that can be shared between threads without hazards, or use persistent data structures with atomic updates based on the atomic variable library (see Atomics).

There are three types of mutexes in Guile: “standard”, “recursive”, and “unowned”.

Calling make-mutex with no arguments makes a standard mutex. A standard mutex can only be locked once. If you try to lock it again from the thread that locked it to begin with (the "owner" thread), it throws an error. It can only be unlocked from the thread that locked it in the first place.

Calling make-mutex with the symbol recursive as the argument, or calling make-recursive-mutex, will give you a recursive mutex. A recursive mutex can be locked multiple times by its owner. It then has to be unlocked the corresponding number of times, and like standard mutexes can only be unlocked by the owner thread.

Finally, calling make-mutex with the symbol allow-external-unlock creates an unowned mutex. An unowned mutex is like a standard mutex, except that it can be unlocked by any thread. A corollary of this behavior is that a thread’s attempt to lock a mutex that it already owns will block instead of signalling an error, as it could be that some other thread unlocks the mutex, allowing the owner thread to proceed. This kind of mutex is a bit strange and is here for use by SRFI-18.

The mutex procedures in Guile can operate on all three kinds of mutexes.

To use these facilities, load the (ice-9 threads) module.

(use-modules (ice-9 threads))

Scheme Procedure: make-mutex [kind]
C Function: scm_make_mutex ()
C Function: scm_make_mutex_with_kind (SCM kind)

Return a new mutex. It will be a standard non-recursive mutex, unless the recursive symbol is passed as the optional kind argument, in which case it will be recursive. It’s also possible to pass unowned for semantics tailored to SRFI-18’s use case; see above for details.

Scheme Procedure: mutex? obj
C Function: scm_mutex_p (obj)

Return #t if obj is a mutex; otherwise, return #f.

Scheme Procedure: make-recursive-mutex
C Function: scm_make_recursive_mutex ()

Create a new recursive mutex. It is initially unlocked. Calling this function is equivalent to calling make-mutex with the recursive kind.

Scheme Procedure: lock-mutex mutex [timeout]
C Function: scm_lock_mutex (mutex)
C Function: scm_timed_lock_mutex (mutex, timeout)

Lock mutex and return #t. If the mutex is already locked, then block and return only when mutex has been acquired.

When timeout is given, it specifies a point in time where the waiting should be aborted. It can be either an integer as returned by current-time or a pair as returned by gettimeofday. When the waiting is aborted, #f is returned.

For standard mutexes (make-mutex), an error is signalled if the thread has itself already locked mutex.

For a recursive mutex (make-recursive-mutex), if the thread has itself already locked mutex, then a further lock-mutex call increments the lock count. An additional unlock-mutex will be required to finally release.

When an asynchronous interrupt (see Asynchronous Interrupts) is scheduled for a thread blocked in lock-mutex, Guile will interrupt the wait, run the interrupts, and then resume the wait.

C Function: void scm_dynwind_lock_mutex (SCM mutex)

Arrange for mutex to be locked whenever the current dynwind context is entered and to be unlocked when it is exited.

Scheme Procedure: try-mutex mx
C Function: scm_try_mutex (mx)

Try to lock mutex and return #t if successful, or #f otherwise. This is like calling lock-mutex with an expired timeout.

Scheme Procedure: unlock-mutex mutex
C Function: scm_unlock_mutex (mutex)

Unlock mutex. An error is signalled if mutex is not locked.

“Standard” and “recursive” mutexes can only be unlocked by the thread that locked them; Guile detects this situation and signals an error. “Unowned” mutexes can be unlocked by any thread.

Scheme Procedure: mutex-owner mutex
C Function: scm_mutex_owner (mutex)

Return the current owner of mutex, in the form of a thread or #f (indicating no owner). Note that a mutex may be unowned but still locked.

Scheme Procedure: mutex-level mutex
C Function: scm_mutex_level (mutex)

Return the current lock level of mutex. If mutex is currently unlocked, this value will be 0; otherwise, it will be the number of times mutex has been recursively locked by its current owner.

Scheme Procedure: mutex-locked? mutex
C Function: scm_mutex_locked_p (mutex)

Return #t if mutex is locked, regardless of ownership; otherwise, return #f.

Scheme Procedure: make-condition-variable
C Function: scm_make_condition_variable ()

Return a new condition variable.

Scheme Procedure: condition-variable? obj
C Function: scm_condition_variable_p (obj)

Return #t if obj is a condition variable; otherwise, return #f.

Scheme Procedure: wait-condition-variable condvar mutex [time]
C Function: scm_wait_condition_variable (condvar, mutex, time)

Wait until condvar has been signalled. While waiting, mutex is atomically unlocked (as with unlock-mutex) and is locked again when this function returns. When time is given, it specifies a point in time where the waiting should be aborted. It can be either a integer as returned by current-time or a pair as returned by gettimeofday. When the waiting is aborted, #f is returned. When the condition variable has in fact been signalled, #t is returned. The mutex is re-locked in any case before wait-condition-variable returns.

When an async is activated for a thread that is blocked in a call to wait-condition-variable, the waiting is interrupted, the mutex is locked, and the async is executed. When the async returns, the mutex is unlocked again and the waiting is resumed. When the thread block while re-acquiring the mutex, execution of asyncs is blocked.

Scheme Procedure: signal-condition-variable condvar
C Function: scm_signal_condition_variable (condvar)

Wake up one thread that is waiting for condvar.

Scheme Procedure: broadcast-condition-variable condvar
C Function: scm_broadcast_condition_variable (condvar)

Wake up all threads that are waiting for condvar.

Guile also includes some higher-level abstractions for working with mutexes.

macro: with-mutex mutex body1 body2 …

Lock mutex, evaluate the body body1 body2 …, then unlock mutex. The return value is that returned by the last body form.

The lock, body and unlock form the branches of a dynamic-wind (see Dynamic Wind), so mutex is automatically unlocked if an error or new continuation exits the body, and is re-locked if the body is re-entered by a captured continuation.

macro: monitor body1 body2 …

Evaluate the body form body1 body2 … with a mutex locked so only one thread can execute that code at any one time. The return value is the return from the last body form.

Each monitor form has its own private mutex and the locking and evaluation is as per with-mutex above. A standard mutex (make-mutex) is used, which means the body must not recursively re-enter the monitor form.

The term “monitor” comes from operating system theory, where it means a particular bit of code managing access to some resource and which only ever executes on behalf of one process at any one time.