本文的主要内容是:

  • 了解goroutine,使用它来运行程序
  • 了解Go是如何检测并修正竞争状态的(解决资源互斥访问的方式)
  • 了解并使用通道chan来同步goroutine

一、使用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都得到执行:

  1. //example1.go
  2. package main
  3. import (
  4. "runtime"
  5. "sync"
  6. "fmt"
  7. )
  8. var (
  9. wg sync.WaitGroup
  10. )
  11. func main() {
  12. //分配一个逻辑处理器P给调度器使用
  13. runtime.GOMAXPROCS(1)
  14. //在这里,wg用于等待程序完成,计数器加2,表示要等待两个goroutine
  15. wg.Add(2)
  16. //声明1个匿名函数,并创建一个goroutine
  17. fmt.Printf("Begin Coroutines\n")
  18. go func() {
  19. //在函数退出时,wg计数器减1
  20. defer wg.Done()
  21. //打印3次小写字母表
  22. for count := 0; count < 3; count++ {
  23. for char := 'a'; char < 'a'+26; char++ {
  24. fmt.Printf("%c ", char)
  25. }
  26. }
  27. }()
  28. //声明1个匿名函数,并创建一个goroutine
  29. go func() {
  30. defer wg.Done()
  31. //打印大写字母表3次
  32. for count := 0; count < 3; count++ {
  33. for char := 'A'; char < 'A'+26; char++ {
  34. fmt.Printf("%c ", char)
  35. }
  36. }
  37. }()
  38. fmt.Printf("Waiting To Finish\n")
  39. //等待2个goroutine执行完毕
  40. wg.Wait()
  41. }

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

  1. Begin Coroutines
  2. Waiting To Finish
  3. 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并行执行呢?为程序指定两个逻辑处理器即可:

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

此时执行程序,输出为:

  1. Begin Coroutines
  2. Waiting To Finish
  3. 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得到执行:

  1. //example2.go
  2. package main
  3. import (
  4. "runtime"
  5. "sync"
  6. "fmt"
  7. )
  8. var (
  9. wg sync.WaitGroup
  10. )
  11. func main() {
  12. //分配一个逻辑处理器P给调度器使用
  13. runtime.GOMAXPROCS(1)
  14. //在这里,wg用于等待程序完成,计数器加2,表示要等待两个goroutine
  15. wg.Add(2)
  16. //声明1个匿名函数,并创建一个goroutine
  17. fmt.Printf("Begin Coroutines\n")
  18. go func() {
  19. //在函数退出时,wg计数器减1
  20. defer wg.Done()
  21. //打印3次小写字母表
  22. for count := 0; count < 3; count++ {
  23. for char := 'a'; char < 'a'+26; char++ {
  24. if char=='k'{
  25. runtime.Gosched()
  26. }
  27. fmt.Printf("%c ", char)
  28. }
  29. }
  30. }()
  31. //声明1个匿名函数,并创建一个goroutine
  32. go func() {
  33. defer wg.Done()
  34. //打印大写字母表3次
  35. for count := 0; count < 3; count++ {
  36. for char := 'A'; char < 'A'+26; char++ {
  37. if char == 'K'{
  38. runtime.Gosched()
  39. }
  40. fmt.Printf("%c ", char)
  41. }
  42. }
  43. }()
  44. fmt.Printf("Waiting To Finish\n")
  45. //等待2个goroutine执行完毕
  46. wg.Wait()
  47. }

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

  1. Begin Coroutines
  2. Waiting To Finish
  3. 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就处于竞争状态下:

  1. //example3.go
  2. package main
  3. import (
  4. "sync"
  5. "runtime"
  6. "fmt"
  7. )
  8. var (
  9. //counter为访问的资源
  10. counter int64
  11. wg sync.WaitGroup
  12. )
  13. func addCount() {
  14. defer wg.Done()
  15. for count := 0; count < 2; count++ {
  16. value := counter
  17. //当前goroutine从线程退出
  18. runtime.Gosched()
  19. value++
  20. counter=value
  21. }
  22. }
  23. func main() {
  24. wg.Add(2)
  25. go addCount()
  26. go addCount()
  27. wg.Wait()
  28. fmt.Printf("counter: %d\n",counter)
  29. }
  30. //output:
  31. counter: 4 或者counter: 2

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

  • 使用原子函数操作
  • 使用互斥锁锁住临界区
  • 使用通道chan

1. 检测竞争状态

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

go build -race example4.go//用竞争检测器标志来编译程序

./example4 //运行程序

工具检测出了程序存在一处竞争状态,并指出发生竞争状态的几行代码是:

  1. 22 counter=value
  2. 18 value := counter
  3. 28 go addCount()
  4. 29 go addCount()

2. 使用原子函数

对整形变量或指针的同步访问,可以使用原子函数来进行。这里使用原子函数来修复example4.go中的竞争状态问题:

  1. //example5.go
  2. package main
  3. import (
  4. "sync"
  5. "runtime"
  6. "fmt"
  7. "sync/atomic"
  8. )
  9. var (
  10. //counter为访问的资源
  11. counter int64
  12. wg sync.WaitGroup
  13. )
  14. func addCount() {
  15. defer wg.Done()
  16. for count := 0; count < 2; count++ {
  17. //使用原子操作来进行
  18. atomic.AddInt64(&counter,1)
  19. //当前goroutine从线程退出
  20. runtime.Gosched()
  21. }
  22. }
  23. func main() {
  24. wg.Add(2)
  25. go addCount()
  26. go addCount()
  27. wg.Wait()
  28. fmt.Printf("counter: %d\n",counter)
  29. }
  30. //output:
  31. counter: 4

这里使用atomic.AddInt64函数来对一个整形数据进行加操作,另外一些有用的原子操作还有:

  1. atomic.StoreInt64() //写
  2. atomic.LoadInt64() //读

更多的原子操作函数请看atomic包中的声明。

3. 使用互斥锁

对临界区的访问,可以使用互斥锁来进行。对于example4.go的竞争状态,可以使用互斥锁来解决:

  1. //example5.go
  2. package main
  3. import (
  4. "sync"
  5. "runtime"
  6. "fmt"
  7. )
  8. var (
  9. //counter为访问的资源
  10. counter int
  11. wg sync.WaitGroup
  12. mutex sync.Mutex
  13. )
  14. func addCount() {
  15. defer wg.Done()
  16. for count := 0; count < 2; count++ {
  17. //加上锁,进入临界区域
  18. mutex.Lock()
  19. {
  20. value := counter
  21. //当前goroutine从线程退出
  22. runtime.Gosched()
  23. value++
  24. counter = value
  25. }
  26. //离开临界区,释放互斥锁
  27. mutex.Unlock()
  28. }
  29. }
  30. func main() {
  31. wg.Add(2)
  32. go addCount()
  33. go addCount()
  34. wg.Wait()
  35. fmt.Printf("counter: %d\n", counter)
  36. }
  37. //output:
  38. counter: 4

使用Lock()Unlock()函数调用来定义临界区,在同一个时刻内,只有一个goroutine能够进入临界区,直到调用Unlock()函数后,其他的goroutine才能够进入临界区。

在Go中解决共享资源安全访问,更常用的使用通道chan。

三、利用通道共享数据

Go语言采用CSP消息传递模型。通过在goroutine之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。这里就需要用到通道chan这种特殊的数据类型。当一个资源需要在goroutine中共享时,chan在goroutine中间架起了一个通道。通道使用make来创建:

  1. unbuffered := make(char int) //创建无缓存通道,用于int类型数据共享
  2. buffered := make(chan string,10)//创建有缓存通道,用于string类型数据共享
  3. buffered<- "hello world" //向通道中写入数据
  4. value:= <-buffered //从通道buffered中接受数据

通道用于放置某一种类型的数据。创建通道时指定通道的大小,将创建有缓存的通道。无缓存通道是一种同步通信机制,它要求发送goroutine和接收goroutine都应该准备好,否则会进入阻塞。

1. 无缓存的通道

无缓存通道是同步的——一个goroutine向channel写入消息的操作会一直阻塞,直到另一个goroutine从通道中读取消息。反过来也是,一个goroutine从channel读取消息的操作会一直阻塞,直到另一个goroutine向通道中写入消息。《Go in action》中关于无缓存通道的解释有一个非常棒的例子:网球比赛。在网球比赛中,两位选手总是处在以下两种状态之一:要么在等待接球,要么在把球打向对方。球的传递可看为通道中数据传递。下面这段代码使用通道模拟了这个过程:

  1. //example6.go
  2. package main
  3. import (
  4. "sync"
  5. "fmt"
  6. "math/rand"
  7. "time"
  8. )
  9. var wg sync.WaitGroup
  10. func player(name string, court chan int) {
  11. defer wg.Done()
  12. for {
  13. //如果通道关闭,那么选手胜利
  14. ball, ok := <-court
  15. if !ok {
  16. fmt.Printf("Player %s Won\n", name)
  17. return
  18. }
  19. n := rand.Intn(100)
  20. //随机概率使某个选手Miss
  21. if n%13 == 0 {
  22. fmt.Printf("Player %s Missed\n", name)
  23. //关闭通道
  24. close(court)
  25. return
  26. }
  27. fmt.Printf("Player %s Hit %d\n", name, ball)
  28. ball++
  29. //否则选手进行击球
  30. court <- ball
  31. }
  32. }
  33. func main() {
  34. rand.Seed(time.Now().Unix())
  35. court := make(chan int)
  36. //等待两个goroutine都执行完
  37. wg.Add(2)
  38. //选手1等待接球
  39. go player("candy", court)
  40. //选手2等待接球
  41. go player("luffic", court)
  42. //球进入球场(可以开始比赛了)
  43. court <- 1
  44. wg.Wait()
  45. }
  46. //output:
  47. Player luffic Hit 1
  48. Player candy Hit 2
  49. Player luffic Hit 3
  50. Player candy Hit 4
  51. Player luffic Hit 5
  52. Player candy Missed
  53. Player luffic Won

2. 有缓存的通道

有缓存的通道是一种在被接收前能存储一个或者多个值的通道,它与无缓存通道的区别在于:无缓存的通道保证进行发送和接收的goroutine会在同一时间进行数据交换,有缓存的通道没有这种保证。有缓存通道让goroutine阻塞的条件为:通道中没有数据可读的时候,接收动作会被阻塞;通道中没有区域容纳更多数据时,发送动作阻塞。向已经关闭的通道中发送数据,会引发panic,但是goroutine依旧能从通道中接收数据,但是不能再向通道里发送数据。所以,发送端应该负责把通道关闭,而不是由接收端来关闭通道。

小结

  • goroutine被逻辑处理器执行,逻辑处理器拥有独立的系统线程与运行队列
  • 多个goroutine在一个逻辑处理器上可以并发执行,当机器有多个物理核心时,可通过多个逻辑处理器来并行执行。
  • 使用关键字 go 来创建goroutine。
  • 在Go中,竞争状态出现在多个goroutine试图同时去访问一个资源时。
  • 可以使用互斥锁或者原子函数,去防止竞争状态的出现。
  • 在go中,更好的解决竞争状态的方法是使用通道来共享数据。
  • 无缓冲通道是同步的,而有缓冲通道不是。

(完)

《Go in action》读后记录:Go的并发与并行的更多相关文章

  1. 《effective Go》读后记录

    一个在线的Go编译器 如果还没来得及安装Go环境,想体验一下Go语言,可以在Go在线编译器 上运行Go程序. 格式化 让所有人都遵循一样的编码风格是一种理想,现在Go语言通过gofmt程序,让机器来处 ...

  2. 《effective Go》读后记录:GO基础

    一个在线的Go编译器 如果还没来得及安装Go环境,想体验一下Go语言,可以在Go在线编译器 上运行Go程序. 格式化 让所有人都遵循一样的编码风格是一种理想,现在Go语言通过gofmt程序,让机器来处 ...

  3. WiscKey: Separating Keys from Values in SSD-Conscious Storage [读后整理]

    WiscKey: Separating Keys from Values in SSD-Conscious Storage WiscKey是一个基于LSM的KV存储引擎,特点是:针对SSD的顺序和随机 ...

  4. 新生 & 语不惊人死不休 —— 《无限恐怖》读后有感

    开篇声明,我博客中“小心情”这一系列,全都是日记啊随笔啊什么乱七八糟的.如果一不小心点进来了,不妨直接关掉.我自己曾经写过一段时间的日记,常常翻看,毫无疑问我的文笔是很差的,而且心情也是瞬息万变的.因 ...

  5. Code Complete 读后总结和新的扩展阅读计划

    Code Complete 读后总结和新的扩展阅读计划 用了一年时间终于将代码大全读完了,在这里做一个简单的总结,并安排下一阶段的扩展阅读计划. 1.选择代码大全作为我程序员职业入门的第一本书,我认为 ...

  6. 《Effective Objective-C 2.0》 读后总结

    感觉自己最近提升很慢了.然后去找了一些面试题看看.发现自己自大了.在实际开发中,让我解决bug.编写功能,我有自信可以完成.但是对项目更深层的思考,我却没有.为了能进到自己的目标BAT.也为了让自己更 ...

  7. 浏览器渲染原理笔记 --《How Browser Work》读后总结

    综述 之前使用ExtJS时遇到一个问题:为什么依次设置多个组件的可见性界面会卡顿?在了解HTML的dom操作相关内容的时候也好奇这个东西到底是怎么回事,然后尤其搞不懂CSS和Html分管样式和网页结构 ...

  8. 在.txt文件的首行写上.LOG后,后面每次对改文本文件进行编辑后,系统会自动在编辑内容后记录操作时间

    在.txt文件的首行写上.LOG后,后面每次对改文本文件进行编辑后,系统会自动在编辑内容后记录操作时间

  9. 深入理解java虚拟机读后总结(个人总结记录)

    1.jvm布局:   jdk1.6版本JVM布局分为:heap(堆),method(方法区),stack(虚拟机栈),native stack(本地方法栈),程序计数器共五大区域. 其中方法区包含运行 ...

随机推荐

  1. 安装python+setuptools+pip+nltk

    环境:Win10 64 + python 2.7 32 bit Source installation (for 32-bit or 64-bit Windows) 1.Install Python: ...

  2. Mvc分页组件MvcSimplePager代码重构及使用

    1 Mvc分页组件MvcSimplePager代码重构 1.1 Intro 1.2 MvcSimplePager 代码优化 1.3 MvcSimplePager 使用 1.4 End Mvc分页组件M ...

  3. JS函数参数

    1.js不是面向对象,不可以重载函数.如果两个函数方法名相同,参数不同,那么js加载时后面的函数会覆盖前面的函数. 所以调用函数时只会调用后面的方法. 2.js设置可变参数时,可以用arguments ...

  4. 扩展GridView实现无数据处理

    提出需求 GridView控件在开发后台管理的时候非常方便快速,但是要实现没有数据时显示“没有数据”,并居中,是一件比较麻烦的事情,这里在一个公开的方法里实现了绑定List<T>和Data ...

  5. 认识大明星——轻量级容器docker知识树点亮

    docker是一个轻量级容器,属于操作系统层面的虚拟化技术,封装了文件系统(AUFS)以及网络互联,进程隔离等特性. 传统虚拟化架构: docker虚拟化架构: 可以看出,docker是没有Guest ...

  6. 边看MHA源码边学Perl语言之一开篇

    边看MHA源码边学Perl语言之一开篇 自我简介 先简单介绍一下自己,到目前为此我已经做了7年左右的JAVA和3年左右php开发与管理,做java时主要开发物流行业的相关软件,对台湾快递,国际快递,国 ...

  7. Java笔记—— 格式化的输入和输出

    精确输出 可以用8个字符的宽度和小数点后了两个字符的精度打印x. double x = 10000.0 /3.0; System.out.printf("%8.2f\n",x);/ ...

  8. 求N个元素的子集合个数

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt406 一个集合有n个元素,请问怎么算出来它的子集(包括空集和本身)是 2的n ...

  9. Servlet总结一

    Servlet总结一 HttpServlet 想要实现一个servlet必须继承这个类,其实一个servlet就是一个java文件,但是这个类必须是继承HttpServlet. 生命周期 servle ...

  10. URL.createObjectURL() 与 URL.revokeObjectURL()

    .URL.createObjectURL URL.createObjectURL()方法会根据传入的参数创建一个指向该参数对象的URL. 这个URL的生命仅存在于它被创建的这个文档里. 新的对象URL ...