sync.Mutex
In Section 8.6, we used a buffered channel as a counting semaphore to ensure that no more than 20 goroutines made simultaneous HTTP requests. With the same idea, we can use a channel of capacity 1 to ensure that at most one goroutine accesses a shared variable at a time. A semaphore that counts only to 1 is called a binary semaphore.
var ( sema = make(chan struct{}, 1) // a binary semaphore guarding balance balance int ) func Deposit(amount int) { sema <- struct{}{} // acquire token balance = balance + amount <-sema // release token } func Balance() int { sema <- struct{}{} // acquire token b := balance <-sema // release token return b }
This pattern of mutual exclusion is so useful that it is
supported directly by the Mutex
type from the sync
package.
Its Lock
method acquires the token (called a lock) and its
Unlock
method releases it:
import "sync" var ( mu sync.Mutex // guards balance balance int ) func Deposit(amount int) { mu.Lock() balance = balance + amount mu.Unlock() } func Balance() int { mu.Lock() b := balance mu.Unlock() return b }
Each time a goroutine accesses the variables of the bank (just
balance
here), it must call the mutex’s Lock
method to
acquire an exclusive lock.
If some other goroutine has acquired the lock, this operation will
block until the other goroutine calls Unlock
and the lock becomes
available again.
The mutex guards the shared variables.
By convention, the variables guarded by a mutex are declared
immediately after the declaration of the mutex itself.
If you deviate from this, be sure to document it.
The region of code between Lock
and Unlock
in which a
goroutine is free to read and modify the shared variables is called a
critical section.
The lock holder’s call to Unlock
happens before any
other goroutine can acquire the lock for itself.
It is essential that the goroutine release the lock once it is
finished, on all paths through the function, including error paths.
The bank program above exemplifies a common concurrency pattern. A set of exported functions encapsulates one or more variables so that the only way to access the variables is through these functions (or methods, for the variables of an object). Each function acquires a mutex lock at the beginning and releases it at the end, thereby ensuring that the shared variables are not accessed concurrently. This arrangement of functions, mutex lock, and variables is called a monitor. (This older use of the word “monitor” inspired the term “monitor goroutine.” Both uses share the meaning of a broker that ensures variables are accessed sequentially.)
Since the critical sections in the Deposit
and Balance
functions are so short—a single line, no branching—calling
Unlock
at the end is straightforward.
In more complex critical sections, especially those in which errors
must be dealt with by returning early, it can be hard to tell that
calls to Lock
and Unlock
are strictly paired on all
paths.
Go’s defer
statement comes to the rescue: by deferring a call
to Unlock
, the critical section implicitly extends to the end
of the current function, freeing us from having to remember to insert
Unlock
calls in one or more places far from the call to
Lock
.
func Balance() int { mu.Lock() defer mu.Unlock() return balance }
In the example above, the Unlock
executes after the return
statement has read the value of balance
, so the Balance
function is concurrency-safe.
As a bonus, we no longer need the local variable b
.
Furthermore, a deferred Unlock
will run even if the critical
section panics, which may be important in programs that make use of
recover
(§5.10).
A defer
is marginally more expensive than an explicit
call to Unlock
, but not enough to justify less clear code. As
always with concurrent programs, favor clarity and resist premature
optimization.
Where possible, use defer
and let critical sections extend to
the end of a function.
Consider the Withdraw
function below.
On success, it reduces the balance by the specified amount and returns
true
.
But if the account holds insufficient funds for the transaction, Withdraw
restores the balance and returns false
.
// NOTE: not atomic! func Withdraw(amount int) bool { Deposit(-amount) if Balance() < 0 { Deposit(amount) return false // insufficient funds } return true }
This function eventually gives the correct result, but it has a nasty
side effect.
When an excessive withdrawal is attempted, the balance transiently
dips below zero.
This may cause a concurrent withdrawal for a modest sum to be
spuriously rejected.
So if Bob tries to buy a sports car,
Alice can’t pay for her morning coffee.
The problem is that Withdraw
is not atomic:
it consists
of a sequence of three separate operations, each of which acquires
and then releases the mutex lock, but nothing locks the whole sequence.
Ideally, Withdraw
should acquire the mutex lock once around the
whole operation.
However, this attempt won’t work:
// NOTE: incorrect! func Withdraw(amount int) bool { mu.Lock() defer mu.Unlock() Deposit(-amount) if Balance() < 0 { Deposit(amount) return false // insufficient funds } return true }
Deposit
tries to acquire the mutex lock a second
time by calling mu.Lock()
, but because mutex locks are not
re-entrant—it’s not possible to lock a mutex that’s already
locked—this leads to a deadlock where nothing can proceed, and
Withdraw
blocks forever.
There is a good reason Go’s mutexes are not re-entrant. The purpose of a mutex is to ensure that certain invariants of the shared variables are maintained at critical points during program execution. One of the invariants is “no goroutine is accessing the shared variables,” but there may be additional invariants specific to the data structures that the mutex guards. When a goroutine acquires a mutex lock, it may assume that the invariants hold. While it holds the lock, it may update the shared variables so that the invariants are temporarily violated. However, when it releases the lock, it must guarantee that order has been restored and the invariants hold once again. Although a re-entrant mutex would ensure that no other goroutines are accessing the shared variables, it cannot protect the additional invariants of those variables.
A common solution is to divide a function such as Deposit
into
two: an unexported function, deposit
, that assumes the lock is
already held and does the real work, and an exported function
Deposit
that acquires the lock before calling deposit
.
We can then express Withdraw
in terms of deposit
like
this:
func Withdraw(amount int) bool { mu.Lock() defer mu.Unlock() deposit(-amount) if balance < 0 { deposit(amount) return false // insufficient funds } return true } func Deposit(amount int) { mu.Lock() defer mu.Unlock() deposit(amount) } func Balance() int { mu.Lock() defer mu.Unlock() return balance } // This function requires that the lock be held. func deposit(amount int) { balance += amount }
Of course, the deposit
function shown here is so trivial that a
realistic Withdraw
function wouldn’t bother calling it, but
nonetheless it illustrates the principle.
Encapsulation (§6.6), by reducing unexpected interactions in a program, helps us maintain data structure invariants. For the same reason, encapsulation also helps us maintain concurrency invariants. When you use a mutex, make sure that both it and the variables it guards are not exported, whether they are package-level variables or the fields of a struct.
3.138.69.45