preface

In some cases, we use sync. map, map+sync.Mutex, or map+sync.RWMutex to avoid exceptions caused by concurrent map writing. If an exception occurs while writing to the map, can I recover it by using recover()?

Note: The Go version used is 1.18

An example of writing a map concurrently

package main

import (
   "fmt"
)

func main(a) {
   m := map[int]bool{}
   go func(a) {
      defer func(a) {
         a := recover()
         fmt.Println("1", a)
      }()
      for {
         m[10] = true
      }
   }()
   go func(a) {
      defer func(a) {
         a := recover()
         fmt.Println("2", a)
      }()
      for {
         m[10] = true
      }
   }()
   select{}}Copy the code

In this example, two Goroutines concurrently write a key to a map. In the function defer+recover() is used to try to catch the exception generated by the concurrent write to the map. The main Goroutine blocks by select{}.

When we run this code, we get the following error message:

fatal error: concurrent map writes goroutine 7 [running]: runtime.throw({0x2c7b35? , 0x2bc2c0? }) < - C: / Program Files/Go/SRC/runtime/panic. Go: 992 + 0 x76 fp = 0 xc000051f68 sp = 0 = 0 x253076 xc000051f38 PCS runtime.mapassign_fast64(0x2b65c0, 0xc000100480, C:/Program Files/Go/ SRC/Runtime /map_fast64.go:177 + 0x2b4fp = 0xC000051fa0 sp= 0xC000051f68 PC = 0x22Fbd4 main.main.func2() D:/Github/him/_test/test.go:24 +0x50 fp=0xc000051fe0 sp=0xc000051fa0 pc=0x2ab770 runtime.goexit() C:/Program Files/Go/src/runtime/asm_amd64.s:1571 +0x1 fp=0xc000051fe8 sp=0xc000051fe0 pc=0x27be81 created by main.main D:/Github/him/_test/test.go:18 +0x7d goroutine 1 [select (no cases)]: main.main() D:/Github/him/_test/test.go:27 +0x85 goroutine 6 [runnable]: main.maiD:/Github/him/_test/test.go:15 +0x50 created by main.main D:/Github/him/_test/test.go:9 +0x50 Process finished with the exit code 2Copy the code

Call runtime.throw() and end the program at line 177 when mapassign_fast64() is called in runtime/map_fast64.go

Mapassign_fast64 ();

func mapassign_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer {
   // omit code...
    
   / / first place
   ifh.flags&hashWriting ! =0 { 
      throw("concurrent map writes")}/ / in the second place
   h.flags ^= hashWriting

   // omit code...
again:
   // omit code...

bucketloop:
   // omit code...

done:
   // omit code...
   / / the third place
   if h.flags&hashWriting == 0 {
      throw("concurrent map writes")}/ / the first around
   h.flags &^= hashWriting
   return elem
}
Copy the code

Among them:

hashWriting  = 4 // Indicates that a Goroutine is writing a map
Copy the code

Our code above only leaves the code associated with hashWriting, which means that a Goroutine is writing a map.

Let’s follow the four positions listed above:

The first place

ifh.flags&hashWriting ! =0 { 
   throw("concurrent map writes")}Copy the code

At the start of the function, check to see if any goroutine is writing to a map. If so, throw()

In the second place

h.flags ^= hashWriting
Copy the code

Here in exclusive or operation, not or operation, because, if there are multiple goroutine through the judgment of the first place, at the same time it can lead to h.f lags that a corresponding hashWriting into 0, so in the third place will be detected

In the third place

if h.flags&hashWriting == 0 {
   throw("concurrent map writes")}Copy the code

In this case, if no map is written concurrently, the result of H. lags&hashWriting should be 1, but since in the concurrent case, The H flags in and not in the hashWriting script (lags in and not in the hashWriting script); the h F lags in and not in the hashWriting script (lags in and not in the hashWriting script);

The first around

h.flags &^= hashWriting
Copy the code

There is a special &^ operation, which has the following rules:

The first operand Second operand The results of explain
0 0 0 & ^ 0 0
0 1 0 0 & ^ 1
1 0 1 1 & ^ 0
1 1 0 1 & ^ 1

The zero lags behind the left operand (except the hashWriting ones) and the one lags behind the left operand (all lags behind the hashWriting ones). The h lags behind the left operand (all lags behind the hashWriting ones) are not affected

The original “H” lags behind the “Written” in the hashWriting and not in the original”

In simple terms

Basically, through ah.flagsTo check if the map is written concurrently, and if so callthrow()

So now we need to see what the throw() does

runtime.throw()

func throw(s string) {
   systemstack(func(a) {
      print("fatal error: ", s, "\n")
   })
   gp := getg()
   if gp.m.throwing == 0 {
      gp.m.throwing = 1
   }
   fatalthrow()
   *(*int) (nil) = 0 // not reached
}
Copy the code
  • First it prints a sentencefatal error: xxxThat’s the first line of output you see above
  • And then we can see it callfatalthrow()So let’s keep watchingfatalthrow()What was implemented

runtime.fatalthrow()

Let’s go straight to the comment for this function:

// fatalthrow implements an unrecoverable runtime throw. It freezes the
// system, prints stack traces starting from its caller, and terminates the
// process.
Copy the code

To put it simply:

  • fatalthrow()Can’t berecover()the
  • It freezes the system, prints the stack, and terminates the process

Fatalthrow () terminates the Process with a call to exit(2), which is the last line in the output above

So we now know that if map detects concurrent writes, throw() is called, and throw() cannot be recovered (), so trying to recover() is useless

conclusion

  • Map checks for concurrent writes
  • Called if concurrent writes are detectedruntime.throw()And cannot berecover()Direct GG,
  • To write maps concurrently, a lock must be placed on the business level (sync.Mutexorsync.RWMutext) or use thesync.MapIsosynchronous container