Goroutine synchronization / channel, mutex, read / write lock, deadlock / condition variable

Posted by stormcloud on Wed, 27 Nov 2019 10:48:41 +0100

1. Goroutine synchronization [data synchronization]

  • Why goroutine synchronization is needed

  • The concept of gorotine synchronization and several ways of synchronization

1.1 why goroutine synchronization is needed

package main

import (
	"fmt"
	"sync"
)

var A = 10
var wg = sync.WaitGroup{}

func Add(){
    defer wg.Done()
    for i:=0;i<1000000;i++{
        A += 1
    }
}

func main() {
	wg.Add(2)
    go Add()
    go Add()
	wg.Wait()
	fmt.Println(A)
}
# output: 
1061865  # Every time we run it, the result is different, but it's not the result we expected 2000000.

Multi goroutine [multitask], with shared resources, and multi goroutine modifies shared resources, resulting in data insecurity [data error], ensuring data security and consistency, and requiring goroutine synchronization

1.2 goroutine synchronization

goroutine executes according to the agreed order to solve the problem of data insecurity.

 

1.3 goroutine synchronization mode

  • channel [csp model]

  • Mutex [traditional synchronization mechanism]

  • Read write lock [traditional synchronization mechanism]

  • Conditional variable [traditional synchronization mechanism]

 

2. Traditional synchronization mechanism

2.1 mutually exclusive lock

2.1.1 characteristics

If the lock is successful, the resource will be operated. If the lock is unsuccessful, the resource will wait until the lock is successful. All goroutine s are mutually exclusive. If one gets the lock, all the others will wait

It solves the problem of data security, reduces the performance of the program, and is suitable for scenarios where reading and writing are not frequent

2.1.2 lock particle size

Granularity refers to the range of locking, where to use resources and where to lock, and minimize the locking range

Basic use process of unit test

  • New unit test file

  • Write test cases

  • The gotest run generates the corresponding prof file

  • go tool to view the generated prof file

package main_test
import (
	"fmt"
	"sync"
	"testing"
)

var A = 10
var wg = sync.WaitGroup{}
var mux sync.Mutex

func Add(){
	defer wg.Done()
	for i:=0;i<1000000;i++{
        mux.Lock()
		A += 1
        mux.Unlock()
	}
}
/*
// Increase lock particle size
func Add(){
	defer wg.Done()
	mux.Lock()
	for i:=0;i<1000000;i++{
		A += 1
	}
	mux.Unlock()
}*/
// Unit test format, 
func TestMux(t *testing.T) {
	wg.Add(2)
	go Add()
	go Add()
	wg.Wait()
	fmt.Println(A)
}

 

# To generate a profile, the - cpuprofile parameter specifies the type of profile to be generated cpu.profile specifies the name of the profile to be generated
go test mutex_test.go -cpuprofile cpu.prof

# View the generated prof file, pprof specifies the file type to view
go tool pprof cpu.prof

# Here is the output
Type: cpu
Time: Jul 10, 2019 at 2:38pm (CST)
Duration: 201.43ms, Total samples = 80ms (39.72%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top  # Here, use the top command to view the cpu usage information in the test
Showing nodes accounting for 80ms, 100% of 80ms total
      flat  flat%   sum%        cum   cum%
      60ms 75.00% 75.00%       60ms 75.00%  sync.(*Mutex).Unlock
      20ms 25.00%   100%       20ms 25.00%  sync.(*Mutex).Lock
         0     0%   100%       80ms   100%  command-line-arguments_test.Add
(pprof) svg  #svg saves the visualization file, which can be visualized by browser
(pprof) list Add  # View detailed time consumption information of corresponding function

 

Be careful:

The current test case is a programming error (this kind of fast computing, a goroutine is already competent, more often read-write separation, mutex lock is not suitable for this frequent read-write scenario), not a lock error

2.1.3 sync.once source reading

// Once is an object that will perform exactly one action.
type Once struct {
	m    Mutex
	done uint32  // Identifies whether the task has been executed. If it is set to 1, the task has been executed
}

// DO calls the method executed by the user, only once
func (o *Once) Do(f func()) {
    // Atomic operation judge done, has been set to 1. If done is 1, it means the method has been executed, and directly returns
	if atomic.LoadUint32(&o.done) == 1 {
		return
	}
   
    // Lock up
	o.m.Lock()
	defer o.m.Unlock()
    // If done is 0, the user function method will be called
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

 

2.2 read write lock

Reading and writing are mutually exclusive, and readers can lock repeatedly. Write lock needs to wait for all readers to unlock. During write lock, all readers wait

It is suitable for scenarios with less writing and more reading. Compared with mutex, it can improve program performance to a certain extent

  • Only readers

 

 

  • Write lock update data

 

 

 

 

 

 

 

2.3 conditional variables

The function of condition variable does not guarantee that only one co program (thread) accesses a shared data resource at the same time, but notifies the co program (thread) blocking on a condition when the state of the corresponding shared data changes. The conditional variable is not a lock, and cannot achieve the goal of synchronization in concurrency. Therefore, the conditional variable is always used together with the lock. It can be considered that the conditional variable is a supplement to the lock, which to some extent improves the efficiency of the lock mechanism

2.3.1 introduction to conditional variable API

  • Create condition variable [cannot be copied after creation]

// Parameter to pass a lock, return pointer type
cond:=sync.NewCond(&sync.Mutex{})

Cond.Wait(), block and release the cup resource on the re condition variable

// Blocking on the condition variable will mount the current gorodine to the Cond queue
cond.Wait()
// 1. Release the lock, Mount yourself to the notification queue, block and wait for [atomic operation]
// 2. Receive wake-up signal and try to acquire lock
// 3. Return if the lock is acquired successfully

Cond.Signal() randomly wakes up a goroutine blocked on a conditional variable

// Wake up the goroutine blocked on the condition variable in the state of wait [calling cond.wait]
// Randomly wakes a thread on the notification queue and removes it from the notification queue
cond.Signal()  // Send wake-up signal

Cond.Broadcast() broadcasts all goroutine s in the wait state

// Broadcast all goroutine s in wait state
// Notify all gorotines on the notification queue and remove all gorotines from the notification queue
cond.Broadcast()

2.3.2 use of conditional variables in the producer consumption model

  • Potential bug -- > deadlock

package main
import "fmt"
import "sync"
import "math/rand"
import "time"

var cond sync.Cond             // Create global condition variable

// Producer
func producer(out chan<- int, idx int) {
   for {
      cond.L.Lock()           	// Condition variable corresponding mutex lock
      for len(out) == 3 {          	// Product area full waiting for consumer consumption
         cond.Wait()             	// Suspend the current process, wait for the condition variable to be met, and wake up by the consumer
      }
      num := rand.Intn(1000) 	// Generate a random number
      out <- num             	// Write to channel (production)
      fmt.Printf("%dth Producers, generating data %3d, Public area surplus%d Data\n", idx, num, len(out))
      cond.L.Unlock()             	// Release the mutually exclusive lock after production
      cond.Signal()           	// Wake up blocked consumers
      time.Sleep(time.Second)       // Rest for a while after production, give other cooperation execution opportunities, and solve the reduction of deadlock opportunities
   }
}
//Consumer
func consumer(in <-chan int, idx int) {
   for {
      cond.L.Lock()           	// The condition variable corresponds to the mutex lock (the same as the producer)
      for len(in) == 0 {      	// Product area is empty waiting for production by producers
         cond.Wait()             	// Suspend the current process, wait for the condition variable to be met, and wake up by the producer
      }
      num := <-in                	// Read (consume) the data in the channel
      fmt.Printf("---- %dth Consumer, Consumption data %3d,Public area surplus%d Data\n", idx, num, len(in))
      cond.L.Unlock()             	// End of consumption, unlock mutually exclusive lock
      cond.Signal()           	// Wake up blocked producers
      time.Sleep(time.Millisecond * 500)    	//Take a rest after consumption, give other cooperation execution opportunities, and solve the reduction of deadlock opportunities
   }
}
func main() {
   rand.Seed(time.Now().UnixNano())  // Set random number seed
   quit := make(chan bool)           // Create a channel to end communication

   product := make(chan int, 3)      // channel simulation for product area (public area)
   cond.L = new(sync.Mutex)          // Creating mutexes and conditional variables

   for i := 0; i < 5; i++ {          // 5 consumers
      go producer(product, i+1)
   }
   for i := 0; i < 3; i++ {          // 3 Producers
      go consumer(product, i+1)
   }
   <-quit                         	// Main process blocking does not end
}
  • An analysis of the causes of deadlock

    1. Extreme processing: 1 producer 2 consumes channle cache 1

    2. In extreme cases, all producers and consumers will enter a wait state, and no one will wake up

  • Solve the bug ---- one way wake up, the producer wakes up the consumer

    Wake up direction problem: wake up from the party with low speed and the party with high speed

package main
import (
	"fmt"
	"runtime"
)
import "sync"
import "math/rand"
import "time"

var cond sync.Cond             // Create global condition variable

// Producer
func producer(out chan<- int, idx int) {
	for {
		num := rand.Intn(1000) 	// Generate a random number
		cond.L.Lock()           	// Condition variable corresponding mutex lock
		select {
		// Attempt to write data to channel
		case out <- num:
			fmt.Printf("%dth Producers, generating data %3d, Public area surplus%d Data\n", idx, num, len(out))
		default:
		}
		cond.L.Unlock()             	// Release the mutually exclusive lock after production
		cond.Signal()           	// Wake up blocked consumers
		runtime.Gosched()			// Give no more chance to create locks
	}
}
//Consumer
func consumer(in <-chan int, idx int) {
	var num int
	for {
		cond.L.Lock()           	// The condition variable corresponds to the mutex lock (the same as the producer)
		for len(in)==0{
			cond.Wait()
		}
		num=<-in
		fmt.Printf("%dth Consumer, consumer %d, Public area surplus%d Data\n", idx, num, len(in))
		cond.L.Unlock()             	// End of consumption, unlock mutually exclusive lock
	}
}
func main() {
	rand.Seed(time.Now().UnixNano())  // Set random number seed
	quit := make(chan bool)           // Create a channel to end communication

	product := make(chan int, 3)      // channel simulation for product area (public area)
	cond.L = new(sync.Mutex)          // Creating mutexes and conditional variables

	for i := 0; i < 3; i++ {          // 3 Producers
		go producer(product, i+1)
	}
	for i := 0; i < 5; i++ {          // 5 consumers 
		go consumer(product, i+1)  
}
	<-quit                         	// Main process blocking does not end
}

 
Question:

When we cancel the conditional variable and use the channel with cache, we can also complete the producer and consumer model well?

2.3.3 channel vs sync.Cond

Using channel to notify goroutine problems with multiple conditions of concern?

The function of off channle and broadcast is only used once

When the state is multiple, the channel fails. Use cond broadcast to update the state

Topics: Go REST Programming less