Several pieces of Go concurrent code

Posted by ErnesTo on Sat, 08 Jan 2022 18:28:07 +0100

Several pieces of Go concurrent code

You can use go run -- race main Go to verify whether there are concurrency problems in the code

for range

for i,v := range slice {
  // ...
  // go func() ...
}

In the for range, i and v variables are initialized only once, and the values of i and v will be modified in each round of the for loop. Therefore, if a goroutine reference i and v is opened in the for loop, because the collaboration of the for range is writing i and v, the internal collaboration of each round of the for loop reads i and v, It can easily lead to concurrency problems.

The solution is to declare a temporary variable inside the for loop, assign the values of I and V to the temporary variable, and the coroutine inside the loop no longer directly references I and V, but refers to the temporary variable.

First version
type T struct {
   FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wg.Add(elementCount)

	for _, t := range sliceT {
		go func() {
			defer wg.Done()
			t.FieldA = "hello"
		}()
	}
	wg.Wait()
}
After repair
type T struct {
	FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wg.Add(elementCount)

	for _, t := range sliceT {
		localT := t
		go func() {
			defer wg.Done()
			localT.FieldA = "hello"
		}()
	}

	wg.Wait()
}
Second version
type T struct {
	FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wg.Add(elementCount)

	for i := range sliceT {
		go func() {
			defer wg.Done()
			sliceT[i].FieldA = "hello"
		}()
	}

	wg.Wait()
}
After repair
type T struct {
	FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wg.Add(elementCount)

	for i := range sliceT {
		localI := i
		go func() {
			defer wg.Done()
			sliceT[localI].FieldA = "hello"
		}()
	}

	wg.Wait()
}

sync.WaitGroup

WaitGroup encapsulates a counter and provides three APIs: Add(), Done(), Wait().

Add(n int) will increment the counter by n.

Done() will decrement the counter by 1.

Wait() will block until the value of the counter becomes 0.

Here is a common error:

type T struct {
	FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}

	for i := range sliceT {
		localI := i
		go func() {
			wg.Add(1)
			defer wg.Done()
			sliceT[localI].FieldA = "hello"
		}()
	}
	
	wg.Wait()
}

This program does not guarantee that the code in go func() will be executed.

Because wg Add (1) is done in the subprocess. The program can completely execute the for loop. After the for loop is completed, it is in wg Wait() returns directly without giving the subprocess a chance to execute. Since the counter in wg is 0, wg Wait() is not blocked.

A very important conclusion is drawn: WG Add() must match WG Wait() is called in the same coroutine.

Package WaitGroup

Sometimes we don't want to call WG explicitly Add() . We just want to execute a few pieces of code concurrently and use WG Wait () waits for these pieces of code to complete execution.

Look at the following program

type T struct {
	FieldA string
}

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}
	
	wg := sync.WaitGroup{}
	wgFunc := func(do func()) {
		wg.Add(1)
		defer wg.Done()
		do()
	}

  for i := range sliceT {
		localI := i
    go wgFunc(func() {
			sliceT[localI].FieldA = "hello"
		})
	}

	wg.Wait()
}

wg is referenced internally in the wgFunc method, and it will execute wg every time it is called Add (1) and defer wg Done () to escort concurrent price protection. Within the for loop, a coroutine will be opened each time to call wgFunc().

But it made the same mistake as the previous code.

The correct packaging should be:

func main() {
	const elementCount = 100
	sliceT := make([]*T, elementCount, elementCount)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wgFunc := func(do func()) {
		wg.Add(1)
		go func() {
			defer wg.Done()
			do()
		}()
	}

	for i := range sliceT {
		localI := i
		wgFunc(func() {
			sliceT[localI].FieldA = "hello"
		})
	}

	wg.Wait()
}

Again, WG Add() must match WG Wait() is executed in the same process.

PS: golang.org/x/sync/errgroup provides more information than sync Waitgroup is a more powerful errgroup Group, use it, use it!

Is it necessary to use the synchronization mechanism when multiple coprocessors read and write the same variable concurrently?

not always.

There is a problem with stack overflow: Can I concurrently write different slice elements

The rule is simple: if multiple goroutines access a variable concurrently, and at least one of the accesses is a write, then synchronization is required.

Structured variables of array, slice, and struct types have elements and fields that may be addressed individually. Each such element acts like a variable.

Each element in the array / each field in the structure can be independently addressed, and they can be regarded as an independent variable!

Therefore, although the above program has many collaborations accessing the same slice, these collaborations access different elements on the slice. And each element on slice is only accessed by one coroutine, so it is safe.

Similarly, it is also safe for multiple coroutines to access different fields of the structure.

type T struct {
	FieldA string
	FieldB string
}

func main() {
	sliceT := make([]*T, 1000)
	for i := range sliceT {
		sliceT[i] = new(T)
	}

	wg := sync.WaitGroup{}
	wgFunc := func(do func()) {
		wg.Add(1)
		go func() {
			defer wg.Done()
			do()
		}()
	}

	for i := range sliceT {
		t := sliceT[i]
		wgFunc(func() {
			t.FieldA = "hello"
		})
		wgFunc(func() {
			t.FieldB = "world"
		})
	}

	wg.Wait()
}

Topics: Go