Golang中的并发控制工具

507 阅读3分钟

1.sync.Mutex

1.1 使用

通过mutex.Lock()mutex.Unlock()来控制代码执行顺序。注意如果锁没有处于上锁状态,执行unlock会报错。"fatal error: sync: unlock of unlocked mutex"

1.2 代码块如下

func main() {
   var mutex sync.Mutex
   mutex.Lock()
   go func() {
      time.Sleep(1 * time.Second)
      fmt.Println("先打印1")
      mutex.Unlock()
   }()

   mutex.Lock()
   fmt.Println("后打印2")

   time.Sleep(2 * time.Second)
}

1.3 执行效果如下

image.png

2.无缓冲channel通道

2.1 使用

首先创建一个无缓冲的通道,done := make(chan int),通过 <-chanchan<-1将数据写入和读取出来。

无缓冲channel如果之前放入了一个值,这个管道里的值没有读取出来使用的话,另一个协程再放入一个值的时候会阻塞。

2.2 代码块如下

func main() {
   done := make(chan int)
   go func() {
      time.Sleep(1 * time.Second)
      fmt.Println("先打印1")
      done <- 1
   }()

   <-done
   fmt.Println("后打印2")
   time.Sleep(2 * time.Second)
}

2.3 执行效果如下

image.png

3. 有缓存channel通道

3.1 使用

和无缓冲channel类似,只不过make函数后面加上了一个缓存大小,不写的时候就是无缓存相当于make(chan int) 等价于 make(chan int,0)

当管道缓冲的时候,管道里的值没有读取出来使用,其他协程也可以放入并且不会有阻塞,除非当缓冲数量达到管道缓冲容量最大值的时候,也会阻塞。

3.2 代码块如下

func main() {
   done := make(chan int, 2)
   go func() {
      time.Sleep(1 * time.Second)
      fmt.Println("先打印1")
      done <- 1
      done <- 1
      done <- 1
      fmt.Println("channel还可以继续放入")
   }()

   <-done
   fmt.Println("后打印2")
   time.Sleep(8 * time.Second)
}

3.3 执行效果如下

image.png

4.sync.WaitGroup

4.1 使用

wg.Add(10)先设置要计数的任务数,各个协程执行完后调用 wg.Done(),在所有任务执行完后,通过wg.Wait()来判断所有任务是否以及完毕,完毕后继续后续代码块执行。

4.2 代码块如下

func main() {
   var wg sync.WaitGroup
   wg.Add(10)
   t1 := time.Now()
   for i := 0; i < 10; i++ {
      go func() {
         random := rand.Intn(4) + 1
         time.Sleep(time.Duration(random) * time.Second)
         fmt.Printf("这个循环等待了%d秒\n", random)
         wg.Done()
      }()
   }
   wg.Wait()
   t2 := time.Now()

   fmt.Println("执行完毕,用时:", (t2.Second() - t1.Second()))
}

4.3 执行效果如下

image.png

5. 基于select实现超时判断退出

5.1 使用

当select有多个分支的时候,会随机选择一个可用的分支。如果没有可用的分支,就用default分支。如果没有default 则会阻塞。

例如有2个分支,一个分支是通过canel管道来执行一段逻辑,然后触发return 退出。另一个分支是用来作为 “如果1秒内还没有收到canel管道的数据写入,则直接退出” 。

需要注意的是,如果select监听的管道被close了,也会触发一个case的执行。(此时如果接受管道的数据,得到的是:bool就是false, int就是0,字符串就是""空字符串,取得对应类型的默认值)

5.2 代码块如下

func main() {
   canel := make(chan bool)
   go func() {
      // 执行正常想要执行的逻辑
      time.Sleep(3 * time.Second)
      close(canel)
   }()

   select {
   case x := <-canel:
      time.Sleep(time.Second)
      fmt.Println("通过执行canel2 ", x)
      return
   case <-time.After(time.Second):
      fmt.Println("执行超时退出")
      return
   }
}

5.3 执行效果如下

image.png

6. context包

6.1 使用

context是专门用来简化单个请求的多个groutine之间于请求域的数据、超时和退出等操作。

context.WithTimeout是创建一个有超时时间设置的context。

image.png

6.2 代码块如下

func main() {
   ctx, canel := context.WithTimeout(context.Background(), 5*time.Second)
   var wg sync.WaitGroup
   wg.Add(3)
   for i := 0; i < 3; i++ {
      go func() {
         defer wg.Done()
         select {
         case <-time.After(6 * time.Second):
            fmt.Println("time.After执行")
         case <-ctx.Done():
            fmt.Println("ctx.Done执行")
         }
      }()
   }
   wg.Wait()
   canel()
   fmt.Println("执行完毕")
   time.Sleep(5 * time.Second)
}

6.3 执行效果如下

image.png


本文正在参加技术专题18期-聊聊Go语言框架