《goinaction》读后记录:go的并发与并行

张伯雨 张伯雨     2022-09-21     608

关键词:

一、使用goroutine来运行程序

1.Go的并发与并行

Go的并发能力,是指让某个函数独立于其他函数运行的能力。当为一个函数创建goroutine时,该函数将作为一个独立的工作单元,被 调度器 调度到可用的逻辑处理器上执行。Go的运行时调度器是个复杂的软件,它做的工作大致是:

  • 管理被创建的所有goroutine,为其分配执行时间
  • 将操作系统线程与语言运行时的逻辑处理器绑定

参考The Go scheduler ,这里较浅显地说一下Go的运行时调度器。操作系统会在物理处理器上调度操作系统线程来运行,而Go语言的运行时会在逻辑处理器上调度goroutine来运行,每个逻辑处理器都分别绑定到单个操作系统线程上。这里涉及到三个角色:

  • M:操作系统线程,这是真正的内核OS线程
  • P:逻辑处理器,代表着调度的上下文,它使goroutine在一个M上跑
  • G:goroutine,拥有自己的栈,指令指针等信息,被P调度

每个P会维护一个全局运行队列(称为runqueue),处于ready就绪状态的goroutine(灰色G)被放在这个队列中等待被调度。在编写程序时,每当go func启动一个goroutine时,runqueue便在尾部加入一个goroutine。在下一个调度点上,P就从runqueue中取出一个goroutine出来执行(蓝色G)。

当某个操作系统线程M阻塞的时候(比如goroutine执行了阻塞的系统调用),P可以绑定到另外一个操作系统线程M上,让运行队列中的其他goroutine继续执行:

上图中G0执行了阻塞操作,M0被阻塞,P将在新的系统线程M1上继续调度G执行。M1有可能是被新创建的,或者是从线程缓存中取出。Go调度器保证有足够的线程来运行所有的P,语言运行时默认限制每个程序最多创建10000个线程,这个现在可以通过调用runtime/debug包的SetMaxThreads方法来更改。

Go可以在在一个逻辑处理器P上实现并发,如果需要并行,必须使用多于1个的逻辑处理器。Go调度器会把goroutine平等分配到每个逻辑处理器上,此时goroutine将在不同的线程上运行,不过前提是要求机器拥有多个物理处理器。

2.创建goroutine

使用关键字go来创建一个goroutine,并让所有的goroutine都得到执行:

//example1.go
package main

import (
   "runtime"
   "sync"
   "fmt"
)

var (
   wg sync.WaitGroup
)

func main() {
   //分配一个逻辑处理器P给调度器使用
   runtime.GOMAXPROCS(1)
   //在这里,wg用于等待程序完成,计数器加2,表示要等待两个goroutine
   wg.Add(2)
   //声明1个匿名函数,并创建一个goroutine
   fmt.Printf("Begin Coroutines\n")
   go func() {
      //在函数退出时,wg计数器减1
      defer wg.Done()
      //打印3次小写字母表
      for count := 0; count < 3; count++ {
         for char := 'a'; char < 'a'+26; char++ {
            fmt.Printf("%c ", char)
         }
      }
   }()
   //声明1个匿名函数,并创建一个goroutine
   go func() {
      defer wg.Done()
      //打印大写字母表3次
      for count := 0; count < 3; count++ {
         for char := 'A'; char < 'A'+26; char++ {
            fmt.Printf("%c ", char)
         }
      }
   }()

   fmt.Printf("Waiting To Finish\n")
   //等待2个goroutine执行完毕
   wg.Wait()

}

这个程序使用runtime.GOMAXPROCS(1)来分配一个逻辑处理器给调度器使用,两个goroutine将被该逻辑处理器调度并发执行。程序输出:

Begin Coroutines
Waiting To Finish
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z 

从输出来看,是先执行完一个goroutine,再接着执行第二个goroutine的,大写字母全部打印完后,再打印全部的小写字母。那么,有没有办法让两个goroutine并行执行呢?为程序指定两个逻辑处理器即可:

//修改为2个逻辑处理器
runtime.GOMAXPROCS(2)

此时执行程序,输出为:

Begin Coroutines
Waiting To Finish
A B C D E a b c d e f g h i j k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z a b c F G H I J K L M N O P Q R S T U V W X d e f g h i j k l m n o p q r s Y Z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 

那如果只有1个逻辑处理器,如何让两个goroutine交替被调度?实际上,如果goroutine需要很长的时间才能运行完,调度器的内部算法会将当前运行的goroutine让出,防止某个goroutine长时间占用逻辑处理器。由于示例程序中两个goroutine的执行时间都很短,在为引起调度器调度之前已经执行完。不过,程序也可以使用runtime.Gosched()来将当前在逻辑处理器上运行的goruntine让出,让另一个goruntine得到执行:

//example2.go
package main

import (
   "runtime"
   "sync"
   "fmt"
)

var (
   wg sync.WaitGroup
)

func main() {
   //分配一个逻辑处理器P给调度器使用
   runtime.GOMAXPROCS(1)
   //在这里,wg用于等待程序完成,计数器加2,表示要等待两个goroutine
   wg.Add(2)
   //声明1个匿名函数,并创建一个goroutine
   fmt.Printf("Begin Coroutines\n")
   go func() {
      //在函数退出时,wg计数器减1
      defer wg.Done()
      //打印3次小写字母表
      for count := 0; count < 3; count++ {
         for char := 'a'; char < 'a'+26; char++ {
            if char=='k'{
               runtime.Gosched()
            }
            fmt.Printf("%c ", char)
         }
      }
   }()
   //声明1个匿名函数,并创建一个goroutine
   go func() {
      defer wg.Done()
      //打印大写字母表3次
      for count := 0; count < 3; count++ {
         for char := 'A'; char < 'A'+26; char++ {
            if char == 'K'{
               runtime.Gosched()
            }
            fmt.Printf("%c ", char)
         }
      }
   }()

   fmt.Printf("Waiting To Finish\n")
   //等待2个goroutine执行完毕
   wg.Wait()

}

两个goroutine在循环的字符为k/K的时候会让出逻辑处理器,程序的输出结果为:

Begin Coroutines
Waiting To Finish
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z A B C D E F G H I J a b c d e f g h i j K L M N O P Q R S T U V W X Y Z A B C D E F G H I J k l m n o p q r s t u v w x y z a b c d e f g h i j K L M N O P Q R S T U V W X Y Z k l m n o p q r s t u v w x y z a b c d e f g h i j k l m n o p q r s t u v w x y z 

这里大小写字母果然是交替着输出了。不过从输出可以看到,第一次输出大写字母时遇到K没有让出逻辑处理器,这是什么原因还不是很清楚,调度器的调度机制?

回到顶部

二、处理竞争状态

并发程序避免不了的一个问题是对资源的同步访问。如果多个goroutine在没有互相同步的情况下去访问同一个资源,并进行读写操作,这时goroutine就处于竞争状态下:

//example3.go
package main

import (
   "sync"
   "runtime"
   "fmt"
)

var (
   //counter为访问的资源
   counter int64
   wg      sync.WaitGroup
)

func addCount() {
   defer wg.Done()
   for count := 0; count < 2; count++ {
      value := counter
      //当前goroutine从线程退出
      runtime.Gosched()
      value++
      counter=value
   }
}

func main() {
   wg.Add(2)
   go addCount()
   go addCount()
   wg.Wait()
   fmt.Printf("counter: %d\n",counter)
}
//output:
counter: 4 或者counter: 2

这段程序中,goroutinecounter的读写操作没有进行同步,goroutine 1对counter的写结果可能被goroutine 2所覆盖。Go可通过如下方式来解决这个问题:

  • 使用原子函数操作
  • 使用互斥锁锁住临界区
  • 使用通道chan
  • 1. 检测竞争状态

    有时候竞争状态并不能一眼就看出来。Go 提供了一个非常有用的工具,用于检测竞争状态。使用方式是:

    go build -race example4.go//用竞争

    《go语言实战》摘录:6.1并发-并行与并发

    6.1并行与并发 查看详情

    go语言学习笔记—进阶—并发编程:轻量级线程goroutine——并发与并行

    并发编程并发指在同一时间内可以执行多个任务。并发编程包含多线程编程、多进程编程、分布式程序等。go语言的并发是指多线程并发,通过goroutine完成goroutine类似线程,可以根据需要创建多个goroutine并发工作goroutine是... 查看详情

    go——并发

     并发与并行的区别: 并发:逻辑上具备同时处理多个任务的能力。 并行:物理上在同一时刻执行多个并发任务。通常都会说程序是并发设计的,也就是说它允许多个任务同时执行,但实际上并不一定真在同一时刻发生。在... 查看详情

    go原生并发原语和最佳实践(代码片段)

    Go编程语言是用并发作为一等公民创建的。它是一种语言,通过抽象出语言中并发原语1背后的并行性细节,您可以轻松编写高度并行的程序。大多数语言都将并行化作为标准库的一部分,或者期望开发者生态系统提供并行化库。... 查看详情

    go系列教程——第20部分:并发入门

    Go是并发式语言,而不是并行式语言。在讨论Go如何处理并发之前,我们必须理解何为并发,以及并发与并行的区别。并发是什么?并发是指立即处理多个任务的能力。一个例子就能很好地说明这一点。我们可以想象一个人正在... 查看详情

    golang语言并发与并行——goroutine和channel的详细理解(代码片段)

    ...会认识到Go有多么的迷人。Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库来新建线程,还要用线程安全的队列库来共享数据。以下是我入门的学习笔记。Go语言的goroutines、信道和死锁goroutin... 查看详情

    go语言并发(代码片段)

    并发与并行并发:同一时间段执行多个任务并行:同一时刻执行多个任务Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时调... 查看详情

    [go]并发和并行的区别

    并发和并行的区别:1.并行是让不同的代码片段同时在不同的物理机器上运行,并行的关键是在不同的物理机器上同时运行2.并发是同时管理很多事情,比如在一个物理机器上进行不停的调度,有些事情可能只做了一半就被暂停... 查看详情

    go语言之并发(代码片段)

    一:并发基础1并发和并行并发和并行是两个不同的概念:1并行意味着程序在任意时刻都是同时运行的:2并发意味着程序在单位时间内是同时运行的详解:  并行就是在任一粒度的时间内都具备同时执行的能力:最简单的并行... 查看详情

    《effectivego》读后记录

    格式化让所有人都遵循一样的编码风格是一种理想,现在Go语言通过gofmt程序,让机器来处理大部分的格式化问题。gofmt程序是go标准库提供的一段程序,可以尝试运行它,它会按照标准风格缩进,对齐,保留注释,它默认使用制... 查看详情

    go语言系列之并发编程(代码片段)

    Go语言中的并发编程并发与并行并发:同一时间段内执行多个任务(你在用微信和两个女朋友聊天)。并行:同一时刻执行多个任务(你和你朋友都在用微信和女朋友聊天)。Go语言的并发通过goroutine实现。goroutine类似于线程,... 查看详情

    [go]并行和并发的区别

    ...行:指在同一时刻,有多条指令在多个处理器上同时执行并发:批在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只有把时间分... 查看详情

    go语言并发,并行,信道(代码片段)

    go语言  并发  并行  信道packagemainimport("fmt""time")补充://并发:看上去在同一时间同时执行,实际是切换执行利用时间片轮转法,同一个CPU进行切换执行//并行:是在真正的同一时间两个程序同时进行吗,这个是在多核cpu... 查看详情

    对于go语言的进阶与依赖管理|青训营笔记(代码片段)

    一.Go语言进阶与依赖管理1.1并发和并行Go可以充分发挥多核优势,高效运行。多线程程序在单核心的cpu上运行,称为并发;多线程程序在多核心的cpu上运行,称为并行。并发与并行并不相同,并发主要由切换时间片来实现“同时... 查看详情

    go协程并发信道

    Go通过协程实现并发,协程之间靠信道通信1.1并发、并行是什么?并行其实很好理解,就是同时执行的意思,在某一时间点能够执行多个任务。想达到并行效果,最简单的方式就是借助多线程或多进程,这样才可在同一时刻执行... 查看详情

    goinaction笔记

    与代码所在文件夹一样的名字作为包名。 当一个标识符的名字以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见。如果一个标识符以大写字母开头,这个标识符就是公开的,即被包外的代码可见。 将工... 查看详情

    goinaction笔记

    与代码所在文件夹一样的名字作为包名。 当一个标识符的名字以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见。如果一个标识符以大写字母开头,这个标识符就是公开的,即被包外的代码可见。 将工... 查看详情

    go语言基础之并发和网络

    ...示Go使用channel和goroutine开发并行程序的能力。goroutine是Go并发能力的核心要素。但是,goroutine到底是什么?叫做goroutine是因为已有的短语——线程、协程、进程等等——传递了不准确的含义。goroutine有简单的模型:它是与其他goroutin... 查看详情