Golang 并发编程 常见错误模板 排雷贴(持续更新中)

454 阅读4分钟

golang 天生支持高并发场景,其核心在于 go routine, go channel, WaitGroup, Mutex 的配合使用。本人前三个月转行 golang,在 go routine, go channel, WaitGroup 使用中遇到许多坑。为此我总结了一些常见的 go 并发编程的错误模板。

1. 忘记向 go routine 传参

本来打算开10个 go routine, 分别打印0-9

package main
import "fmt"

func main() {
   for i := 0; i < 10; i++ {
      go func() {
         fmt.Println(i)
      }()
   }
 
time.Sleep(2 * time.Second)
 
}

点击运行后,发现10次输出并不是0到9各输出一次,而是某个数字被输出了很多次, 最小的输出并不是0 ,最大的输出甚至是 10。其原因在于部分 go routine 开启后正准备去获取 i, 而此时 i 已经 经过了 好几轮 for 循环了。

解决方法: 向 go routine 内部传参, 每开 一个 go routine,就往里面传参。

package main
import "fmt"

func main() {
  for i := 0; i < 10; i++ {
     go func(i int) {
        fmt.Println(i)
     }(i)
  }
  time.Sleep(2 * time.Second)
}

2. 不带缓冲的 go channel 读写时机不一致 导致死锁

package main
import "fmt"

func main() {
   c := make(chan string)
   c <- "hello,world"
   s := <-c
   fmt.Println(s)
}

点击运行, 直接报错死锁。其原因在于,对于不带缓冲的 go channel 必需读操作 先于写操作,这样做的目的是防止 go channel 里面的东西 没有人去读。

解决方法1:新开一个 go routine 提前准备好读取 go channel

package main
import "fmt"

func main() {
   c := make(chan string)

   go func(c chan string) {
      s := <-c
      fmt.Println(s)
   }(c)

   c <- "hello,world"

}

解决方法2: 使用带缓冲的 go channel

package main
import "fmt"

func main() {
   c := make(chan string, 1)
   c <- "hello,world"
   s := <-c
   fmt.Println(s)

}

3. 忘记使用 WaiGroup 三件套: Add, Done, Wait 方法,导致主线程提前结束。

package main
import "fmt"
 
func main() {
   for i := 0; i < 10; i++ {
      go func(i int) {
         fmt.Println(i)
      }(i)
   }

}

解决方法: 使用WaiGroup 三件套。

WaitGroup是用来等待协程完成的 一个类。其常用的方法有三个,add, donewait

WaitGroup.add(1) 表示告诉 主线程 等待一个 gorouinte 完成才能结束, 要不然主线程早早结束了,而go toutine 里面的任务还没有完成。比如在代码的 for 循环里面,每开一个 go routine 之前,都要调用 WaitGroup.add(1)方法, 表示告诉主线程先别着急结束,我这里又有一个新任务,你先等着。

WaitGroup.Done() 表示告诉主线程, 我这里的go routine执行结束了,你不用等我了。你可以将 WaitGroup.Done() 看作是 WaitGroup.add(-1) 而 defer WaitGroup.Done() 表示在 这个 go routine 结束之前一定要告诉外面这个 go routine 结束了。defer 表示最后一步执行。

WaitGroup.Wait() 表示主线程在这里等待所有 go routine 完成任务,后面的代码先不执行。

package main

import (
   "fmt"
   "sync"
)

func main() {
   wg := sync.WaitGroup{}
   
   for i := 0; i < 10; i++ {
      wg.Add(1)
      go func(i int) {
         defer wg.Done()
         fmt.Println(i)
      }(i)
   }

   wg.Wait()

}

4. channel 忘记关闭, 没有 go routine往里写, 但是有 go routine 一直在读, 导致死锁。

package main
import (
   "fmt"
   "sync"
)

func main() {
   c := make(chan int, 10)
   var wg sync.WaitGroup

   wg.Add(1)
   go func() {
   
      defer wg.Done()
      for i := 0; i < 10; i++ {
         c <- i
      }
   }()

   wg.Add(1)
   go func() {
      defer wg.Done()
      for x := range c {
         fmt.Println(x)
      }
   }()
   wg.Wait()

}

第一个 go routine 往channel里面写入10个 int, 第二个channel一直从 channel 读,当channel为空时, 这个 go routine 还一直在等待(因为 wg.Wait)

解决方法: 第一个 go routine 执行完以后, 主动关闭 channel, 这样第二个 go routine 在读的时候如果发现 channel 已经被关闭了 就会结束。

package main
import (
   "fmt"
   "sync"
)

func main() {
   c := make(chan int, 10)
   var wg sync.WaitGroup

   wg.Add(1)
   go func() {
      defer close(c)
      defer wg.Done()
      for i := 0; i < 10; i++ {
         c <- i
      }
   }()

   wg.Add(1)
   go func() {
      defer wg.Done()
      for x := range c {
         fmt.Println(x)
      }
   }()
   wg.Wait()

}

5. 两个 go channel 你读我,我读你,相互等待死锁。

package main
import "sync"

func main() {
   ch1 := make(chan int,0)
   ch2:= make(chan int,0)

   wg := sync.WaitGroup{}

   wg.Add(1)
   go func() {
      defer wg.Done()
      select {
      case <- ch1:
         ch2<- 100
      }
   }()

   wg.Add(1)
   go func() {
      defer wg.Done()
      select {
      case <- ch2:
         ch1 <- 100
      }
   }()

   wg.Wait()
}

解决方法:给ch1 或者 ch2 任意一个channel 初始化写入一个数

package main
import (
   "fmt"
   "sync"
)

func main() {
   ch1 := make(chan int,1)
   ch2:= make(chan int,1)

   wg := sync.WaitGroup{}



   wg.Add(1)
   go func() {
      defer wg.Done()
      temp:=<- ch1
      ch2 <- temp
      fmt.Println("Read from ch1 and then send to ch2" )
   }()

   wg.Add(1)
   go func() {
      defer wg.Done()
      temp:=<- ch2
      ch1 <- temp
      fmt.Println("Read from ch2 and then send to ch1" )
   }()

   // 下面两行代码注释掉一行
   //ch1<-1
   ch2<-2

   wg.Wait()
}

6. channel 没有被写入 , select 语句中不写 defafult 造成死锁

 
package main

import (
   "fmt"
)

func main() {
   c1 := make(chan int, 1)
   c2 := make(chan int, 1)

   select {
   case int1 := <-c1:
      fmt.Println("c1 received: ", int1)
   case int2 := <-c2:
      fmt.Println("c2 received: ", int2)
     
   }

}
 

解决方法, 写入 channel, 或者增加default 语句

package main

import (
   "fmt"
)

func main() {
   c1 := make(chan int, 1)
   c2 := make(chan int, 1)

   c2 <- 1 // 这条语句和 default 语句注释一条即可

   select {
   case int1 := <-c1:
      fmt.Println("c1 received: ", int1)
   case int2 := <-c2:
      fmt.Println("c2 received: ", int2)
   default: // default 语句 和 c2<-1 注释一条即可
      fmt.Println("No channel received.")
   }

}

7. 多个 go routine 对同一个 数据进行修改不上锁,造成写冲突

package main

import (
   "fmt"
   
   "time"
)

func main() {
   var count int

   for i := 0; i < 1000; i++ {
      go func() {
         count = count + 1
         time.Sleep(10)
      }()
   }
   time.Sleep(time.Second)
   fmt.Println(count)

}

预计输出 1000,但每次运行结果不一样,原因是 修改 count变量的 go routine 还没来得及操作就被 另一个 go routine 抢先操作。

解决方法,对 count 变量进行上锁。

package main

import (
   "fmt"
   "sync"
   "time"
)

func main() {
   var count int
   var mu sync.Mutex

   for i := 0; i < 1000; i++ {
      go func() {
         mu.Lock()
         count = count + 1
         mu.Unlock()
         time.Sleep(10)
      }()
   }
   time.Sleep(time.Second)
   fmt.Println(count)

}