Shared Memory in Go
In concurrent programming, shared memory occurs when many entities, like Go’s goroutines, directly access and modify the same memory regions. By using shared variables or data structures, this enables these entities to interact and share information.
Go’s Idiomatic Approach to Shared Memory
Go promotes a concurrency idiom that can be summed up in the slogan “Don’t communicate by sharing memory, share memory by communicating,” whereas shared memory is a conventional communication technique in multi-threaded programming. According to this theory, goroutines should mostly use channels to transmit and receive data rather than directly altering shared variables. This method aims to avoid data race situations, which happen when several goroutines attempt to access and change the same shared value at the same time, producing erratic and frequently incorrect outcomes. With regard to the result, “all bets are off” when two or more goroutines attempt to use a shared item simultaneously.
Race condition detection in your code is made easier by the Go compiler’s features, including the -race flag for go tool compile or go run. Fixing any reported races is highly encouraged.
The Monitor Goroutine Pattern
In line with its “share memory by communicating” tenet, Go’s most popular and reliable method of managing shared state is through a structure commonly referred to as a monitor goroutine or service loop. With this pattern:
- One goroutine is assigned to “own” a particular piece of shared data. This indicates that the information is contained inside the scope of that goroutine and is not directly available to other people.
- Through channels, other goroutines that must communicate with the owner goroutine in order to access or alter this shared data do so indirectly.
- Since only the owner goroutine directly modifies the data, this method effectively serializes access and removes the possibility of race situations by design, preventing data corruption by nature.
- In order to wait for events or commands on channels, a worker goroutine is frequently constructed as a for loop with a choose statement.
By concentrating on creating transparent communication channels rather than controlling locks around shared data, this technique streamlines concurrent programming.
Code Example: Monitor Goroutine
package main
import (
"fmt"
"math/rand"
"os"
"strconv"
"sync"
"time"
)
// readValue is a channel for reading the shared value.
var readValue = make(chan int)
// writeValue is a channel for writing a new value to the monitor.
var writeValue = make(chan int)
// set sends a new value to the monitor goroutine.
func set(newValue int) {
writeValue <- newValue // Send the new value to the writeValue channel
}
// read requests and returns the current value from the monitor goroutine.
func read() int {
return <-readValue // Receive the current value from the readValue channel
}
// monitor is the goroutine that owns and manages the shared 'value' variable.
func monitor() {
var value int // 'value' is local to monitor(), ensuring it's owned by this goroutine
for {
select {
case newValue := <-writeValue: // If a new value is sent on writeValue channel
value = newValue // Update the shared value
fmt.Printf("%d ", value) // Print the updated value
case readValue <- value: // If a read request comes, send the current value
// The monitor sends its 'value' to the readValue channel
}
}
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Please give an integer!")
return
}
n, err := strconv.Atoi(os.Args[26])
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("Going to create %d random numbers.\n", n)
rand.Seed(time.Now().Unix()) // Seed the random number generator
go monitor() // Start the monitor goroutine
var w sync.WaitGroup // Use a WaitGroup to ensure all goroutines complete
for i := 0; i < n; i++ {
w.Add(1) // Increment the WaitGroup counter for each goroutine
go func() {
defer w.Done() // Decrement the WaitGroup counter when the goroutine finishes
set(rand.Intn(10 * n)) // Call set() to modify the shared value via the monitor
}()
}
w.Wait() // Wait for all goroutines to complete
fmt.Println("\nLast value:", read()) // Read and print the final value via the monitor
fmt.Println("Finished.")
}
In this instance, the monitor() goroutine completely encapsulates the value variable. If another goroutine needs to access or change this value, it must use the writeValue or readValue channels to send or receive data, accordingly. The “sharing by communicating” concept is demonstrated by this serialisation of access, which also avoids racial conditions. The way these read and write requests are handled is coordinated by the select statement in monitor().
You can also read Select Statement GoLang Is A Strong Control Structure