互斥锁(sync.Mutex)的使用

  多个协程并发执行可能会产生数据竞争问题,如下面的代码就有数据竞争问题:

package main

import (
   "fmt"
   "time"
)

func main() {
   num := 0

   // 创建100000个协程,每个协程对num进行一次自增运算
   for i := 0; i < 100000; i++ {
      go func(i int) {
         num++ // num自增1
         fmt.Printf("协程%d将num自增1,此时num的值为%d \n", i, num)
      }(i)
   }

   time.Sleep(time.Second * 15) // 由于协程比较多,所以休眠15秒让所有协程都能执行
}

// ========== 输出过程·开始 ========== //
// ……
// 协程99990将num自增1,此时num的值为99009
// 协程99992将num自增1,此时num的值为99010 <<<<<
// 协程99991将num自增1,此时num的值为99010 <<<<<
// 协程99993将num自增1,此时num的值为99011
// 协程99995将num自增1,此时num的值为99013
// 协程99994将num自增1,此时num的值为99012
// 协程99996将num自增1,此时num的值为99014
// 协程99998将num自增1,此时num的值为99015
// 协程99999将num自增1,此时num的值为99017 <<<<<
// 协程99997将num自增1,此时num的值为99017 <<<<<
// ========== 输出过程·结束 ========== //

// ========== 总结 ========== //
// 1、想知道代码有没有数据竞争问题可以在执行“go run”时加入“-race”参数,即:go run -race main.go,
//    如果有数据竞争问题会出现“exit status 66”。


 
  在协程中加锁防止出现数据竞争:

package main

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

func main() {
   num := 0
   lock := sync.Mutex{}

   // 创建100000个协程,每个协程对num进行一次自增运算
   for i := 0; i < 100000; i++ {
      go func(i int) {
         lock.Lock() // 加锁

         num++ // num自增1
         fmt.Printf("协程%d将num自增1,此时num的值为%d \n", i, num)

         lock.Unlock() // 解锁
      }(i)
   }

   time.Sleep(time.Second * 15) // 由于协程比较多,所以休眠15秒让所有协程都能执行
}

// ========== 输出过程·开始 ========== //
// ……
// 协程99990将num自增1,此时num的值为99991
// 协程99991将num自增1,此时num的值为99992
// 协程99992将num自增1,此时num的值为99993
// 协程99994将num自增1,此时num的值为99994
// 协程99993将num自增1,此时num的值为99995
// 协程99995将num自增1,此时num的值为99996
// 协程99996将num自增1,此时num的值为99997
// 协程99998将num自增1,此时num的值为99998
// 协程5967将num自增1,此时num的值为99999
// 协程99999将num自增1,此时num的值为100000
// ========== 输出过程·结束 ========== //


 
  也可以使用管道来解决协程间的数据竞争问题【推荐方式】:

package main

import (
   "fmt"
   "time"
)

func main() {
   num := 0
   channel := make(chan struct{}, 0) // 创建一个无缓冲管道(容量为0的管道)用于协程之间通信

   // 创建100000个协程,每个协程对num进行一次自增运算
   for i := 0; i < 100000; i++ {
      go func(i int) {
         <-channel // 从管道读取数据(重要提醒:这里会发生阻塞,直到读取到数据才会解除阻塞)

         num++ // num自增1
         fmt.Printf("协程%d将num自增1,此时num的值为%d \n", i, num)

         channel <- struct{}{} // 往管道写入空结构体,解除下一个协程的阻塞
      }(i)
   }

   channel <- struct{}{} // 往管道写入空结构体,解除第一个协程的阻塞

   time.Sleep(time.Second * 15) // 由于协程比较多,所以休眠15秒让所有协程都能执行

   close(channel) // 关闭管道
}

// ========== 输出过程·开始 ========== //
// ……
// 协程99976将num自增1,此时num的值为99991
// 协程99974将num自增1,此时num的值为99992
// 协程99985将num自增1,此时num的值为99993
// 协程99998将num自增1,此时num的值为99994
// 协程99913将num自增1,此时num的值为99995
// 协程99999将num自增1,此时num的值为99996
// 协程99993将num自增1,此时num的值为99997
// 协程99992将num自增1,此时num的值为99998
// 协程99837将num自增1,此时num的值为99999
// 协程99607将num自增1,此时num的值为100000
// ========== 输出过程·结束 ========== //

// ========== 总结 ========== //
// 1、在创建100000个协程后,由于有“<-channel”所以这些协程都会发生阻塞,直到“channel <- struct{}{}”解除第一个协程的阻塞,
//    然后产生连锁使得协程阻塞逐个解除并执行num自增操作,从而解决数据竞争问题。
// 2、这里值得一提的是管道是无缓冲的(容量为0),且数据类型是空结构体,这里这么定义管道是基于以下两点:
//    (1) 协程之间只是需要通过管道传递信号,并不关心管道的数据类型和值,所以不占用内存空间的空结构体是最佳选择。
//    (2) 无缓冲管道(容量为0)不需要使用内存来存储数据,能让协程之间的通信变得更加高效和低延迟。

Copyright © 2024 码农人生. All Rights Reserved