This is the 21st day of my participation in the August Text Challenge.More challenges in August

Continuing our understanding of concurrency in Golang today, we look at the problem of critical resources under concurrency.

Critical resources

Critical resources: Resources shared by multiple processes/threads/coroutines in a concurrent environment.

However, when critical resources are not handled properly in concurrent programming, data inconsistency often results.

Sample code:

package main

import (
	"fmt"
	"time"
)

func main(a)  {
	a := 1
	go func(a) {
		a = 2
		fmt.Println("Child goroutine...",a)
	}()
	a = 3
	time.Sleep(1)
	fmt.Println("Main goroutine...",a)
}
Copy the code

We do this by terminal command:

The ability to find a single piece of data shared by multiple Goroutines.

Critical resource security issues

Concurrency itself is not complicated, but the problem of resource competition makes it more complicated to develop good concurrent programs, which can cause a lot of puzzling problems.

If multiple Goroutines access the same data resource and one thread changes the data, this value is modified, and may not be correct for other Goroutines.

For example, we implement the train station ticketing program by concurrency. There are 100 tickets available at four outlets.

Let’s take a look at the sample code:

package main

import (
	"fmt"
	"math/rand"
	"time"
)

// Global variables
var ticket = 10 / / 100 tickets

func main(a) {
	/* 4 goroutines, simulate 4 ticket outlets, 4 subroutines operate on the same shared data. * /
	go saleTickets("Box 1") / / g1, 100
	go saleTickets("Box 2") / / g2, 100
	go saleTickets("Box 3") / / g3, 100
	go saleTickets("Box 4") / / g4, 100

	time.Sleep(5*time.Second)
}

func saleTickets(name string) {
	rand.Seed(time.Now().UnixNano())
	//for i:=1; i<=100; i++{
	// FMT.Println(name," sold: ", I)
	/ /}
	for { //ticket=1
		if ticket > 0 { //g1,g3,g2,g4
			/ / sleep
			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
			// g1 ,g3, g2,g4
			fmt.Println(name, "Sold:", ticket)  // 1, 0, -1, -2
			ticket--   //0, -1,-2, -3
		} else {
			fmt.Println(name,"Sold out. No tickets left.")
			break}}}Copy the code

In order to better observe the critical resource problem, each Goroutine first sleeps a random number, and then sells tickets. We found that the running result of the program can also sell tickets with negative numbers.

Analysis:

Our tickets logic is to first determine whether the number of votes is negative, if greater than zero, then we have tickets, but in selling tickets money to sleep first, then sell, if say at this point has been selling tickets to the only one, one goroutine holds the CPU time slice, it fragments to see whether they have a ticket, condition is true, So it can sell the last ticket numbered one. But because it sleeps before it sells, the other Goroutine will hold the CPU’s time slice, and the second goroutine will decide if it has a ticket, so it can sell the ticket, but it also goes to sleep. The other three goroutines and the fourth goroutine all follow the same logic. When one goroutine wakes up, it doesn’t tell if there are any tickets, it just sells the last ticket, and then the other goroutines wake up and sell the 0 ticket, -1 ticket, -2 ticket.

This is the problem of the insecurity of critical resources. When a goroutine accesses a data resource, it determines the condition by value, and then another Goroutine preempts the resource and changes the value. When the goroutine continues to access the data, the value is not correct.

Solution of critical resource security problem

To solve the problem of critical resource security, the solution of many programming languages is synchronization. By locking, only one Goroutine is allowed to access the shared data at any given time. After the current goroutine is unlocked, other Goroutines can access the shared data.

We can use the lock operation under sync package.

Sample code:

package main

import (
	"fmt"
	"math/rand"
	"time"
	"sync"
)

// Global variables
var ticket = 10 / / 100 tickets

var wg sync.WaitGroup
var matex sync.Mutex // Create a lock

func main(a) {
	/* 4 goroutines, simulate 4 ticket outlets, 4 subroutines operate on the same shared data. * /
	wg.Add(4)
	go saleTickets("Box 1") / / g1, 100
	go saleTickets("Box 2") / / g2, 100
	go saleTickets("Box 3") / / g3, 100
	go saleTickets("Box 4") / / g4, 100
	wg.Wait()              // main waits for...

	//time.Sleep(5*time.Second)
}

func saleTickets(name string) {
	rand.Seed(time.Now().UnixNano())
	defer wg.Done()
	//for i:=1; i<=100; i++{
	// FMT.Println(name," sold: ", I)
	/ /}
	for { //ticket=1
		matex.Lock()
		if ticket > 0 { //g1,g3,g2,g4
			/ / sleep
			time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
			// g1 ,g3, g2,g4
			fmt.Println(name, "Sold:", ticket) // 1, 0, -1, -2
			ticket--                         //0, -1,-2, -3
		} else {
			matex.Unlock() / / unlock
			fmt.Println(name, "Sold out. No tickets left.")
			break
		}
		matex.Unlock() / / unlock}}Copy the code

Running results:

There’s a classic saying in concurrent programming for Go: Don’t communicate as shared memory, communicate as shared memory.

Sharing information between different Goroutines (using shared memory to communicate) is discouraged in Go by locking the shared state. Instead, the shared state or changes in the shared state are encouraged to be passed between goroutines (communicating with shared memory) through channels, which also act like locks to ensure that only one Goroutine accesses the shared state at a time.

Of course, mainstream programming languages provide a basic set of synchronization tools, such as locks, condition variables, atomic operations, and so on, to ensure the security and consistency of data shared between multiple threads. Not surprisingly, the Go language standard library provides these synchronization mechanisms in much the same way as other languages.