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 canceled, 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 signaling 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))
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.
Return #t
if obj is a mutex; otherwise, return
#f
.
Create a new recursive mutex. It is initially unlocked. Calling this
function is equivalent to calling make-mutex
with the
recursive
kind.
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 signaled 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.
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.
Try to lock mutex and return #t
if successful, or #f
otherwise. This is like calling lock-mutex
with an expired
timeout.
Unlock mutex. An error is signaled 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.
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.
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.
Return #t
if mutex is locked, regardless of ownership;
otherwise, return #f
.
Return a new condition variable.
Return #t
if obj is a condition variable; otherwise,
return #f
.
Wait until condvar has been signaled. 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
signaled, #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.
Wake up one thread that is waiting for condvar.
Wake up all threads that are waiting for condvar.
Guile also includes some higher-level abstractions for working with mutexes.
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.
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.