Go Language Study Notes - Chapter 9 Concurrent Programming Based on Shared Variables (Traditional) (The Go Programming Language)

Chapter 9 Concurrent Programming Based on Shared Variables (Traditional)

9.1 Race Conditions

focus

  • Exported package-level functions are generally concurrency-safe.
  • . Since package-level variables cannot be restricted to a single goroutine, modifying these variables "must" use mutual exclusion conditions.
  • Reasons why it cannot work when calling concurrently: such as deadlock (deadlock), livelock (livelock) and starvation (resource starvation)
  • A race condition refers to a program that does not give correct results when multiple goroutines interleave operations
  • A data race occurs whenever two goroutines access the same variable concurrently, and at least one of them is a write operation. There are three ways to avoid write races:
    • The first way is not to write variables
    • The second way to avoid data races is to avoid accessing variables from multiple goroutines
    • A third way to avoid data races is to allow many goroutines to access variables, but at most one goroutine is accessing them at a time. This approach is called "mutual exclusion".
  • Go's mantra:Don't use shared data to communicate; use communication to share data
  • A goroutine that provides requests for a specified variable through a cahnnel is called a monitor goroutine for that variable.
  • It is a common behavior to share variables between goroutines on a pipeline, and address information is transmitted through channels between the two. This rule is sometimes called serial binding.

Common libraries and methods

  • image.Image

9.2sync.Mutex mutual exclusion lock

focus

  • Use a channel with a capacity of only 1 to ensure that at most one goroutine accesses a shared variable at the same time. A semaphore that can only be 1 and 0 is called a binary semaphore.
  • Mutex (sync.Mutex). Its Lock method can obtain the token (here called a lock), and the Unlock method will release the token.
  • By convention, variables protected by a mutex are declared immediately after the mutex variable declaration. If your practice does not conform to convention, make sure to explain your practice in the documentation.
var (
mu sync.Mutex // guards balance
balance int
)
  • The content goroutine in the code segment between Lock and Unlock can be read or modified at will. This code segment is called a critical section.
  • Each function acquires a mutex at the beginning and releases the lock at the end, thus ensuring that shared variables cannot be accessed concurrently. This arrangement of functions, mutexes, and variables is called a monitor.
  • We use defer to call Unlock, the critical section will implicitly extend to the end of the function scope
  • deferred UnlockpanicIt will still recoverexecute even when the critical section occurs , which is very important for the program used for recovery
  • The defer call will only cost a little more than calling Unlock explicitly, but it ensures the cleanliness of the code to a large extent.
  • There is no way to lock a mutex that has already been locked - this would cause the program to deadlock.

Common libraries and methods

  • sync.Mutex

9.3sync.RWMutex read-write lock

focus

  • Multiple read-only operations are allowed to execute in parallel, but write operations are completely mutually exclusive. This kind of lock is called "multiple readers, single writer lock". The lock provided by Go language issync.RWMutex
  • RLock can only be used when there is no write operation to the critical section shared variable.
  • RWMutex requires more complex internal records, so it is slower than a normal uncontended lock mutex

Common libraries and methods

  • sync.RWMutex sync.RWMutex.RLock() sync.RWMutex.RUnlock

9.4 Memory Synchronization

focus

  • If two goroutines are executed on different CPUs, and each core has its own cache, the writes of one goroutine will not be visible to the other goroutine's Print until the main memory is synchronized.
  • All concurrency problems can be avoided with consistent, simple, established patterns. So if possible, limit the variable inside the goroutine; if it is a variable that multiple goroutines need to access, use mutual exclusion conditions to access it.

9.5sync.Once initialization

focus

  • Lazy initialization (lazy initialization), can save initialization time
func loadIcons() {
icons = map[string]image.Image{
"spades.png": loadIcon("spades.png"),
"hearts.png": loadIcon("hearts.png"),
"diamonds.png": loadIcon("diamonds.png"),
"clubs.png": loadIcon("clubs.png"),
}
}
// NOTE: not concurrency-safe!
func Icon(name string) image.Image {
if icons == nil {
loadIcons() // one-time initialization
}
return icons[name]
}
  • The above will cause problems in concurrent calls. Therefore, when a goroutine checks whether the icons are not empty, it cannot just assume that the initialization process of this variable has been completed. It may be like this
func loadIcons() {
icons = make(map[string]image.Image)
icons["spades.png"] = loadIcon("spades.png")
icons["hearts.png"] = loadIcon("hearts.png")
icons["diamonds.png"] = loadIcon("diamonds.png")
icons["clubs.png"] = loadIcon("clubs.png")
}
  • sync.OnceIt is used to solve the one-time initialization problem.
  • One-time initialization requires a mutex mutex and a boolean variable to record whether the initialization has been completed; the mutex is used to protect the boolean variable and the client data structure.

Common libraries and methods

  • sync.Once sync.Once.Do

9.6 Race Condition Detection

focus

  • Go's runtime and tool chain equip us with a complex but useful dynamic analysis tool. The race detector can effectively help us debug errors in concurrent programs.
  • Simply appending the -race flag to the go build, go run, or go test commands will cause the compiler to create a "modified" version of your application or a test with tools that record all runtime accesses to shared variables. , and will record the identity information of each goroutine that reads or writes shared variables.
  • The complete set of synchronization events is described in The Go Memory Model documentation, which is kept together with the language documentation.

9.7 Example: Concurrent non-blocking cache

focus

  • Concurrent, non-repetitive, non-blocking cache, through the broadcast mechanism of the channel, notifies other goroutines to read the value of the goroutine in time, and the broadcast mechanism is completed through close

9.8 Goroutines and Threads

focus

  • The difference between goroutine and operating system threads is actually just a quantitative difference

9.8.1 Dynamic stack

focus

  • Each OS thread has a fixed-size memory block (usually 2MB) as a stack, and this stack will be used to store the internal variables of the function that is currently being called or suspended (when calling other functions). A 2MB stack is a big waste of memory for a small goroutine, but it is obviously not enough for more complex or deeper recursive function calls. A goroutine will start its life cycle with a very small stack, which generally only needs 2KB, and the size of the stack will be dynamically expanded according to the needs, and the maximum value is 1GB.

9.8.2 Goroutine Scheduling

focus

  • A hardware timer interrupts the processor, which calls a kernel function called the scheduler.
  • Here is a paragraph that introduces the scheduling mechanism of the OS kernel, which leads to the m:n scheduling of goroutines, that is, multiplexing (scheduling) m goroutines on n operating system threads.
  • The work of the Go scheduler is similar to the scheduling of the kernel, but this scheduler only focuses on the goroutine in a single Go program
  • The thread scheduling difference of the operating system is that the Go scheduler does not use a hardware timer but is scheduled by the Go language "architecture" itself.
  • For example, when a goroutine calls time.Sleep or is blocked by a channel call or a mutex operation, the scheduler will make it sleep and start executing another goroutine until the time comes to wake up the first goroutine.
  • Because this scheduling method does not need to enter the context of the kernel, rescheduling a goroutine is much cheaper than scheduling a thread.

Common libraries and methods

  • os.Signal signal.Notify os.Interrupt

9.8.3GOMAXPROCS

focus

  • GOMAXPROCS variable to determine how many operating system threads will execute Go code at the same time.
  • Its default value is the number of CPU cores on the running machine, so on a machine with 8 cores, the scheduler will schedule GO code on 8 OS threads at a time. (GOMAXPROCS is the n in the m:n schedule mentioned above).
  • A goroutine that is sleeping or blocked in communication does not need a corresponding thread for scheduling. In I/O or system calls or when calling non-Go language functions, a corresponding operating system thread is required, but GOMAXPROCS does not need to count these cases.
  • This parameter can be controlled explicitly with the GOMAXPROCS environment variable, or
    runtime.GOMAXPROCSit can be modified in a runtime function.
  • The scheduling of goroutine is affected by many factors, and the runtime is constantly evolving, so the actual results you get here may be different from the results we run due to different versions.

Common libraries and methods

  • runtime.GOMAXPROCS

9.8.4 Goroutine has no ID number

focus

  • Thread-local storage (thread local storage, content that other threads do not want to access in multi-threaded programming) is very easy. You only need a map with the thread id as the key to solve the problem, and each thread can use its id from it. The value is obtained and does not conflict with other threads.
  • Goroutines have no concept of an identity (id) that can be obtained by the programmer. This is intentional by design, since thread-local storage can always be abused.

Guess you like

Origin blog.csdn.net/rabbit0206/article/details/103758530