Go mutex (sync.Mutex) and read / write lock (sync.RWMutex)

Posted by lazaruz on Tue, 01 Mar 2022 13:11:46 +0100

When do I need a lock?

When there is only one thread in the program, there is no need to lock, but usually the actual code is not just a single thread, so the lock needs to be used at this time. What are the main scenarios related to the use of the lock?

  • When multiple threads are reading the same data
  • When multiple threads write the same data
  • The same resource can be read and written

Mutex (sync.Mutex)

Mutex lock is a common method to control the access of shared resources. It can ensure that only one goroutine can access the shared resources at the same time (only one thread can get the lock at the same time)

First, an example of concurrent reading and writing is used to demonstrate what happens when multiple threads access global variables at the same time?

package main
import ("fmt")
 
var count int
 
func main() {
    for i := 0; i < 2; i++ {
        go func() {
            for i := 1000000; i > 0; i-- {
                count ++
            }
            fmt.Println(count)
        }()
    }
 
    fmt.Scanf("\n")  //Wait for all child threads to end
}
 
Operation results:
980117
1011352  //The final result is unlikely to be what we want to see: 200000

By modifying the code and adding mutexes to the accumulation, we can ensure that the result we get every time is the desired value

package main
import ("fmt"
    "sync"
)

var (
    count int
    lock sync.Mutex
)

func main() {
    for i := 0; i < 2; i++ {
        go func() {
            for i := 1000000; i > 0; i-- {
                lock.Lock()
                count ++
                lock.Unlock()
            }
            fmt.Println(count)
        }()
    }

    fmt.Scanf("\n")  //Wait for all child threads to end
}

Operation results:
1952533
2000000  //The last print output of the thread

Read write lock (sync.RWMutex)

In the environment of more reading and less writing, the read-write mutex (sync.RWMutex) can be used preferentially, which is more efficient than mutex. RWMutex in sync package provides encapsulation of read-write mutex

Read write locks are divided into read locks and write locks

  • If a write lock is set, other read threads and write threads cannot get the lock. At this time, it has the same function as the mutex lock
  • If a read lock is set, other write threads cannot get the lock, but other read threads can get the lock

Data consistency can also be achieved by setting write lock:

package main
import ("fmt"
    "sync"
)
 
var (
    count int
    rwLock sync.RWMutex
)
 
func main() {
    for i := 0; i < 2; i++ {
        go func() {
            for i := 1000000; i > 0; i-- {
                rwLock.Lock()
                count ++
                rwLock.Unlock()
            }
            fmt.Println(count)
        }()
    }
 
    fmt.Scanf("\n")  //Wait for all child threads to end
}
 
Operation results:
1968637
2000000

Performance comparison between mutex lock and read-write lock

demo: make an example of more reading and less writing, open three goroutine s for reading and writing respectively, and output the final reading and writing times

1) Use mutex:

package main
import (
    "fmt"
    "sync"
    "time"
)
 
var (
    count  int
    //mutex 
    countGuard sync.Mutex
)
 
func read(mapA map[string]string){
    for {
        countGuard.Lock()
        var _ string = mapA["name"]
        count += 1
        countGuard.Unlock()
    }
}
 
func write(mapA map[string]string) {
    for {
        countGuard.Lock()
        mapA["name"] = "johny"
        count += 1
        time.Sleep(time.Millisecond * 3)
        countGuard.Unlock()
    }
}
 
 
 
func main() {
    var num int = 3
    var mapA map[string]string = map[string]string{"nema": ""}
 
    for i := 0; i < num; i++ {
        go read(mapA)
    }
 
    for i := 0; i < num; i++ {
        go write(mapA)
    }
 
    time.Sleep(time.Second * 3)
    fmt.Printf("Final reads and writes:%d\n", count)
}
 
Operation results:
Final reading and writing times: 3766

2) Use read-write lock

package main
import (
    "fmt"
    "sync"
    "time"
)
 
var (
    count  int
    //Read write lock
    countGuard sync.RWMutex
)
 
func read(mapA map[string]string){
    for {
        countGuard.RLock()  //A read lock is defined here
        var _ string = mapA["name"]
        count += 1
        countGuard.RUnlock()
    }
}
 
func write(mapA map[string]string) {
    for {
        countGuard.Lock()  //A write lock is defined here
        mapA["name"] = "johny"
        count += 1
        time.Sleep(time.Millisecond * 3)
        countGuard.Unlock()
    }
}
 
 
 
func main() {
    var num int = 3
    var mapA map[string]string = map[string]string{"nema": ""}
 
    for i := 0; i < num; i++ {
        go read(mapA)
    }
 
    for i := 0; i < num; i++ {
        go write(mapA)
    }
 
    time.Sleep(time.Second * 3)
    fmt.Printf("Final reads and writes:%d\n", count)
}
 
Operation results:
Final reading and writing times: 8165

conclusion

As a result, the gap is about 2 times, and the efficiency of reading locks is much faster!

Supplement on mutex

Problems needing attention in mutex:

  1. Do not lock mutexes repeatedly
  2. Don't forget to unlock the mutex and use the defer statement if necessary
  3. Do not pass mutexes directly between multiple functions

Deadlock: the main goroutines in the current program and those goroutines we have enabled have been blocked. These goroutines can be called user level goroutines, which means that the whole program has stalled, and at this time, the go program will throw the following panic:

fatal error: all goroutines are asleep - deadlock!

And the panic thrown by the system when the go language is running belongs to fatal errors, which can not be recovered. Calling the recover function has no effect on them

The mutex in Go language is used out of the box, that is, we declare a sync Mutex type variable can be used directly. Note: this type is a structure type and belongs to a value type. Passing it as a parameter to a function, returning it from the function, assigning it to other variables and letting it enter a pipeline will lead to the generation of its copy. Moreover, the original value is completely independent from the copy and multiple copies. They are different mutually exclusive locks, so the lock should not be passed through the parameters of the function.

Supplement on read-write lock

  1. Attempting to lock the write lock again when the write lock has been locked will block the current goroutine
  2. Attempting to lock the read lock again when the write lock is locked will also block the current goroutine
  3. Attempting to lock the write lock while the read lock is locked will also block the current goroutine
  4. Attempting to lock the read lock after the read lock has been locked will not block the current goroutine

For a shared resource protected by a read-write lock, multiple write operations cannot be performed at the same time, and write operations and read operations cannot be performed at the same time, but multiple read operations can be performed at the same time

Unlocking the write lock will wake up "all goroutine s blocked by trying to lock the read lock", and this usually makes them successfully lock the read lock (this is not understood yet)

Unlocking the read lock will only wake up the "goroutine blocked due to trying to lock the write lock" on the premise that there are no other read lock locks. Only one goroutine can successfully lock the write lock, and other goroutines have to wait in place. As for which goroutine, it depends on who waits for the longest event

Unlocking the unlocked write lock in the read-write lock will immediately trigger panic. The same is true for the read lock, which is also unrecoverable.

Original link:
https://www.cnblogs.com/kaichenkai/p/11108303.html
https://www.cnblogs.com/zhaof/p/8636384.html

Topics: Go