This is the 26th day of my participation in the August More Text Challenge

The mutex

The Mutex mode is widely used, so the Sync package has a separate Mutex type to support it. Its Lock method is used to acquire tokens (a process also known as locking), and its Unlock method is used to release tokens (unlocking). Again, in the bank transfer example from the previous article, locks are used to ensure concurrent security

package bank

import "sync"

var (
    mu sync.Mutex
    balance int
)

func Deposit(amount int)  {
    mu.Lock()
    balance = balance + amount
    mu.Unlock()
}

func Balance() int {
    mu.Lock()
    b := balance
    mu.Unlock()
    return b
}
Copy the code

Every time a Goroutine accesses a bank variable (only balance in this case), it must first call the mutex Lock method to obtain a mutex. If another Goroutine has already taken the mutex, the operation is blocked until another Goroutine calls UNLOCK (at which point the mutex is available again). Mutex protects shared variables. By convention, variable declarations protected by mutex should follow the declaration of the mutex

Code between Lock and Unlock is free to read and modify shared variables. This area is called the critical area. No other Goroutine can acquire a lock until the lock holder calls Unlock. So it is important that goroutine releases the lock as soon as it is used, and that all branches of the function, especially the error branch, be included

The above banking program shows a typical concurrency pattern. Several export functions (global, capitalized, cross-package accessible) encapsulate one or more variables, which can then be accessed only through these functions (or methods for an object’s variables). Each function ensures that shared variables cannot be accessed concurrently by claiming a mutex at the beginning and releasing it at the end. This combination of functions, mutex, and variables is called monitor mode. (The term monitor was also used earlier in monitoring Goroutine, which stands for using a broker to ensure that variables are accessed in sequence.)

Because the critical regions in the Deposit and Balance functions are short (only one line and no branches), it is also convenient to call Unlock directly at the end of the function. In more complex critical scenarios, especially where errors must be handled by returning early, it is difficult to be sure that Lock and Unlock are executed in pairs in all branches. The Go defer statement solves this problem: by delaying Unlock execution, you implicitly extend the critical region to the end of the current function, avoiding the need to insert an Unlock statement at one or more locations away from the Lock

func Balance() int {
    mu.Lock()
    defer mu.Unlock()
    return balance
)
Copy the code

In the example above, the Unlock is executed after the return statement has read the balance variable, so the balance function is concurrency safe. Also, you don’t need to use the local variable B

Also, Unlock delayed execution in the event of a critical region crash will execute correctly, which is especially important if RECOVER is used (for exception handling in Go, click here). When dealing with concurrent programs, clarity should always be a priority and premature optimization should always be rejected. Where available, use defer to extend the critical area to the end of the function

Take a look at the Withdraw function below. When successful, the balance is reduced by the specified amount and returns true, but if the balance is insufficient to complete the transaction, Withdraw restores the balance and returns false

Func Withdraw(amount int) bool {Deposit(-amount) if Balance() < 0 {Deposit(amount) return false} return true}Copy the code

This function will eventually give the correct result, but it has a side effect. When you overdraw, at some point, the balance drops below $0. This could result in a small withdrawal being logically rejected. So, when B tries to buy A sports car, A can’t afford A cup of coffee. The problem with Withdraw is that it is not atomic. It itself contains three serial operations, each claiming and releasing the mutex, but not locking the entire sequence

There is an incorrect way to lock the entire operation

func Withdraw(amount int) bool { mu.Lock() defer mu.Unlock() Deposit(-amount) if Balance() < 0 { Deposit(amount) return } return true}Copy the code

Deposit is called mu.lock. But since the mutex is not reentrant (you can’t relock an already locked mutex), this will cause the deadlock Withdraw to remain stuck.

Go mutex is non-reentrant, as explained later. The purpose of mutex is to maintain specific invariants based on shared variables during program execution. One of the invariants is “No Goroutine is accessing this shared variable”, but it is possible that mutex also protects other invariants against data structures. When Goroutine acquires a mutex, it may assume that these invariants are satisfied. When it acquires the mutex, it may update the value of the shared variable, which may temporarily not satisfy the previous invariant. When it releases the mutex, it must ensure that the previous invariants have been restored and satisfied again. Although a reentrant mutex ensures that no other Goroutine can access shared variables, it does not protect other invariants of those variables

A common solution is to split functions such as Deposit into two parts: a non-exporting (private) function Deposit, which assumes the mutex has been acquired and does the actual business logic; And an exported (public) function Deposit, which gets the lock and calls Deposit. So we can use deposit to Withdraw:

func Deposit(amount int) { mu.Lock() defer mu.Unlock() deposit(amount) } func Withdraw(amount int) bool { mu.Lock() Defer mu.unlock () deposit(-amount) if Balance() < 0 {deposit(amount) return false} return true} func Balance() Func deposit(amount int) {balance += amount} func deposit(amount int) {balance += amount}Copy the code

Encapsulation, which helps us ensure invariants in data structures by reducing unintended interactions with them in our programs. Encapsulation can also be used to maintain invariance in concurrency for similar reasons. So when you use a mutex, either to protect package-level variables or fields in a structure, make sure that neither the mutex itself nor the protected variables are exported

Read and write mutex

B felt anxious when he found that his 100 yuan deposit had disappeared without leaving any clues. To solve the problem, B wrote a program to check his account balance hundreds of times per second. The program runs simultaneously at his home, at work and on his phone. Banks notice that the rapid growth of business requests is slowing down deposit and withdrawal operations because all Balance requests run serally, holding mutex and temporarily blocking other Goroutines

Because the Balance function only reads the state of the variable, multiple Balance requests can safely run concurrently, as long as Deposit and Withdraw requests are not running at the same time. In this scenario, a special type of lock is required that allows read-only operations to be executed concurrently, but writes require full exclusive access. This lock is called a multiread/write lock, and sync.rwmutex in Go provides this functionality:

Var mu sync.rwmutex var balance int func balance () int {mu.rlock () // Defer mu.runlock () return balance}Copy the code

The Balance function can now call the RLock and RUnlock methods to acquire and release a read lock (also known as a shared lock), respectively. The Deposit function does not need to be changed. It obtains and releases a write Lock (also known as a mutex) by calling mu.Lock and mu.unlock, respectively.

After the above changes, most of B’s Balance requests can be run in parallel and complete faster. As a result, locks are available for a larger percentage of time and Deposit requests are responded to more promptly

RLock can only be used when there is no write to a shared variable within a critical region. In general, we should not assume that functions and methods that are logically read-only will not update some variables. For example, a method that looks like a simple accessor might increment the counters used internally, or update a cache to make repeated calls faster

RWMutex has an advantage only if the vast majority of Goroutines acquire read locks and lock competition is fierce (i.e., goroutines typically wait to acquire locks). Because RWMutex requires more complex internal bookkeeping, it is slower than normal mutex when competition is low

reference

The Go Programming Language — Alan A. A. Donovan

Go Language Learning Notes — Rain Marks