Go语言 并发


Go语言中的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为goroutine时,Go会将其视为一个独立的工作单元。

操作系统会在物理处理器上调度线程来运行,而Go语言运行时会在逻辑处理器上调度 goroutine 来运行。每个逻辑处理器都分别绑定到单个操作系统线程。如果创建一个 goroutine 并运行,那么这个 goroutine 就会被放到调度器的全局运行队列中,之后调度器就会将这些队列中的 goroutine 分配给一个逻辑处理器,并将其放到这个逻辑处理器对应的本地运行队列中。本地运行队列中的 goroutine 会一直等待直到自己被分配的逻辑处理器执行。

goroutine 协程
我们通过一个在逻辑处理器上运行的例子来理解调度器的行为与如何管理 goroutine。

func main() {
   //分配一个逻辑处理器给调度器使用
   runtime.GOMAXPROCS(1)
   //wg 用来等待线程完成
   var wg sync.WaitGroup
   //Add(2)表示要等待两个goroutine
   wg.Add(2)
   fmt.Println("start goroutines")

   //声明匿名函数,并创建一个goroutine
   go func() {
      //在函数退出时调用Done来通知main函数工作已经完成
      defer wg.Done()
      for i := 0;i<10;i++{
         fmt.Println("func1: ",i)
      }
   }()

   //声明匿名函数,并创建一个goroutine
   go func() {
      //在函数退出时调用Done来通知main函数工作已经完成
      defer wg.Done()
      for i := 20;i<30;i++{
         fmt.Println("func2: ",i)
      }
   }()

   fmt.Println("Wating to finish")
   //等待所以 goroutine 结束
   //这里如果不设置就有可能会导致main函数在运行两个goroutine完成之前提前退出,这样程序就有可能提前终止
   wg.Wait()
}

函数说明:
runtime.GOMAXPROCS(num):允许程序更改调度器可以使用的逻辑处理器的数量,如: runtime.GOMAXPROCS(runtime.NumCPU()),为每个可用的物理处理器创建一个逻辑处理器。

sync.WaitGroup:是一个计数信号量,可以用来记录并维护运行 goroutine 。如果WaitGroup的值大于0,就会导致 wg.Wait() 被阻塞,每执行一次wg.Done(),WaitGroup的值就会减一,最终为0.

基于调度器的内部算法,一个正在运行的 goroutine 可能会被停止并重新调度,这样的目的是为了防止某个goroutine 长时间占用逻辑处理器。如果多个goroutine 在没有互相同步的情况下访问某个共享的资源,那么就可能产生竞态。

竞态检测器

go build -race //竞态检测器
./example //运行程序

来看一下竞态的小示例

var(
   counter int
   wg sync.WaitGroup
)

func main() {
   wg.Add(2)

   go IncCounter(1)
   go IncCounter(2)
   wg.Wait()
   fmt.Println(counter)
}

func IncCounter(id int)  {
   defer wg.Done()

   for count:=0;count<2;count++{
      value := counter
     //让出对处理器的占用
      runtime.Gosched()
      value ++
      counter = value
   }
}

输出结果并不一定等于4,这是因为每个 goroutine 都会覆盖掉原来 goroutine 的工作内容,也就是说当第一个goroutine 进入到IncCounter时,运行到 runtime.Gosched()时,会让出对处理器的占用,然后让第二个 goroutine 进入到 IncCounter执行,这样就会导致原来的 counter 被覆盖掉,最终结果就会出现错误。

同步工具
①原子函数:从底层的加锁机制来同步访问整型变量和指针
将上面的函数改为原子函数操作

func IncCounter(id int)  {
   defer wg.Done()
   for count:=0;count<2;count++{
      atomic.AddInt64(&counter,1)
//放弃对处理器的占用,回到队列中,相当于java中的yeild
      runtime.Gosched()
   }
}

②互斥锁
互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码。

func IncCounter(id int) {
   defer wg.Done()
   for count := 0; count < 2; count++ {
      mutex.Lock()
      {
         value := counter
         runtime.Gosched()
         value ++
         counter = value
      }
      mutex.Unlock()
   }
}

③通道
除了上面两个方式进行同步消除竞态以外,还可以使用通道来解决竞争问题。
当一个资源需要在 goroutine 中被共享时,通道会在goroutine之间建立一个管道,并提供同步交换数据的机制。声明通道时,需要指定被共享的数据类型。通道分为无缓冲的通道与有缓冲的通道。向通道发送数据需要用到<-操作符

court := make(chan int) //无缓冲通道
court := make(chan int,10)//有缓冲通道
court<- 1 //向通道发送整数 1
value,ok := <-court //从通道中接收数据,value:接收的通道数据,ok:通道是否被关闭,true表示正常开启

无缓冲的通道是指接收前没有能力保存任何值的通道,这种类型的通道要求发送 goroutine 与接收 goroutine 要同时准备好,如果没有同时准备好,那么先发送或者先接收的 goroutine 就会进行阻塞等待。

//无缓冲通道示例
var wg1 sync.WaitGroup

//init初始化包,Go语言运行时会在其他代码执行前优先执行这个
func init()  {
   //设置随机数种子
   rand.Seed(time.Now().UnixNano())
}
func main(){
   court := make(chan int)
   wg1.Add(2)
   go player("A",court)
   go player("B",court)
   //开始发球
   court<- 1
}

func player(name string,court chan int)  {
   defer wg1.Done()
   for {
      ball,ok := <-court //ok:表示接收到的值有效
      if !ok{
         //说明通道被关闭
         fmt.Printf("name: %v is win\n",name)
         return
      }
      n := rand.Intn(100)
      if n%13 == 0{
         fmt.Printf("Player %v is missed\n",name)
         //通道已被关闭
         close(court)
         return
      }
      fmt.Printf("Player %v Hit %v\n",name,ball)
      ball++
      //将球打回对方
      court <- ball
   }
}

有缓冲的通道是指在通道数据被接收前有能力保存一个或多个的值,这种类型的通道并不要求发送 goroutine 与接收 goroutine 要同时准备好。只有在通道中没有多余的缓存空间保存值的时候,发送 goroutine 的动作才会进行阻塞,同理只有在通道中没有要接收的值时,接收动作才会被阻塞。如果缓冲区已满,再往通道发送数据时就会报错

func main() {
   ch := make(chan int, 1)
   ch <- 1
   ch <- 2
   fmt.Println(<-ch)
   fmt.Println(<-ch)
}
----output----
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    D:/GoDemo/src/MyGo/Demo_03.go:8 +0x7a

与无缓冲通道相比不同点:无缓冲通道能保证通道中发送与接收动作是同时进行的,而有缓冲的通道则不会保证。

通道关闭:
在有缓存的通道执行通道关闭( close(court))后,goroutine依旧可以从通道中接收数据,这样有利于将缓存中的所有数据都接收,从而不会使得数据丢失,但是并不能往通道中发送数据。只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有值需要发送的时候才有必要关闭,例如终止一个 range 循环。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容