前言

在我前面一篇文章Golang受欢迎的原因中已经提到,Golang是在语言层面(runtime)就支持了并发模型。那么作为编程人员,我们在实践Golang的并发编程时,又有什么需要注意的点呢?下面我会跟大家详细的介绍一些在实际生产编程中很容易踩坑的知识点。

CSP

在介绍Golang的并发实践前,有必要先介绍简单介绍一下CSP理论。CSP,全称是Communicating sequential processes,翻译为通信顺序进程,又翻译为交换消息的顺序程序,用来描述并发性系统的交互模式。CSP有以下三个特点:

1.每个程序是为了顺序执行而创建的

2.数据通过管道来通信,而不是通过共享内存

3.通过增加相同的程序来扩容

Golang的并发模型基于CSP理论,Golang并发的口号是:不用通过共享内存来通信,而是通过通信来共享内存。

Golang并发模式

Golang用来支持并发的元素集:

  • goroutines
  • channels
  • select
  • sync package

其中goroutines,channels和select 对应于实现CSP理论,即通过通信来共享内存。这几乎能解决Golang并发的90%问题,另外的10%场景需要通过同步原语来解决,即sync包相关的结构。

看图识channel

如上图所示,我们从一个简单的沙桶传递小游戏来认识Golang中的channel。其中蓝色的Gopher为发送方,紫色的Gopher为接受方,中间的灰色Gopher代表channel的缓冲区大小。

channel介绍

阻塞channel

不带buffer的channel阻塞情况

  1. unbuffered := make(chan int)
  2.  
  3. a := <- unbuffered // 阻塞
  4.  
  5. unbuffered := make(chan int)
  6.  
  7. // 1) 阻塞
  8.  
  9. a := <- unbuffered
  10.  
  11. // 2) 阻塞
  12.  
  13. unbuffered <-
  14.  
  15. // 3) 同步
  16.  
  17. go func() { <-unbuffered }()
  18.  
  19. unbuffered <-

带buffer的channel阻塞情况

  1. buffered := make(chan int, )
  2.  
  3. // 4) 阻塞
  4.  
  5. a := <- buffered
  6.  
  7. // 5) 不阻塞
  8.  
  9. buffered <-
  10.  
  11. // 6) buffer满,阻塞
  12.  
  13. buffered <-

上述情况其实归纳起来很简单:不管有无缓冲区channel,写满或者读空都会阻塞。

不带buffer和带buffer的channel用途:

  • 不带buffer的channel:用于同步通信。
  • 带buffer的channel:用于异步通信。

关闭channel

  1. c := make(chan int)
  2.  
  3. close(c)
  4.  
  5. fmt.Println(<-c) //接收并输出chan类型的零值,这里int是0 

需要特殊说明的是,channel不像socket或者文件,不需要通过close来释放资源。需要close的唯一情况是,通过close触发channel读事件,comma,ok := <- c 中ok为false,表示channel已经关闭。只能在发送端close channel,因为channel关闭接收端能感知到,但是发送端感知不到,只能主动关闭。往已经关闭的channel发送信息将会触发panic。

select

类似switch语句,只不过case都是channel的读或者写操作,也可能是default。case的顺序一点都不重要,不要依赖case的先后来定义优先级,第一个非阻塞(send and/or receive)的case将会被选中。

使channel不阻塞

  1. func TryReceive(c <-chan int) (data int, more, ok bool) {
  2.  
  3.   select {
  4.  
  5.   case data, more = <- c:
  6.  
  7.     return data, more, true
  8.  
  9.   }
  10.  
  11.   default:
  12.  
  13.     return , true, false
  14.  
  15. }

当select中的case都处于阻塞状态时,就会选中default分支。

或者超时返回:

  1. func TryReceiveWithTimeout(c <-chan int, duration time.Duration) (data int, more, ok bool) {
  2.  
  3.   select {
  4.  
  5.   case data, more = <-c:
  6.  
  7.     return data, more, true
  8.  
  9.   case <- time.After(duration):
  10.  
  11.     return , true, false
  12.   }
  13. }

time.After(duration)会返回一个channel,当duration到期时会触发channel的读事件。

Channel的缺点:

1.Channel可能会导致死锁(循环阻塞)

2.channel中传递的都是数据的拷贝,可能会影响性能

3.channel中传递指针会导致数据竞态问题(data race/ race conditions)

第三点中提到了数据竞态问题,也就是通常所说data race。在接着往下讲之前有必要先简单讲解下data race的危害。data race 指的是多线程并发读写一个变量,对应到Golang中就是多个goroutine同时读写一个变量,这种行为是未定义的,也就是说读变量出来的值很有可能不是写入的值,这个值是任意值都有可能。

例如下面这段代码:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "runtime"
  6. "time"
  7. )
  8.  
  9. var i int64 =
  10.  
  11. func main() {
  12. runtime.GOMAXPROCS()
  13. go func() {
  14. for {
  15. fmt.Println("i is", i)
  16. time.Sleep(time.Second)
  17. }
  18. }()
  19.  
  20. for {
  21. i +=
  22. }
  23. }

在我mac本地环境会不断的输出0。全局变量i被两个goroutine同时读写,也就是我们所说的data race,导致了i的值是未定义的。如果读写的是一块动态伸缩的内存,很有可能会导致panic。例如多goroutine读写map。幸运的是,Golang针对data race有专门的内置工具,例如把上面的代码保存为main.go,执行 go run -race main.go 会把相关的data race输出:

  1. ==================
  2.  
  3. WARNING: DATA RACE
  4.  
  5. Read at 0x00000121e848 by goroutine :
  6.  
  7. main.main.func1()
  8.  
  9. /Users/saas/src/awesomeProject/datarace/main.go: +0x3e
  10.  
  11. Previous write at 0x00000121e848 by main goroutine:
  12.  
  13. main.main()
  14.  
  15. /Users/saas/src/awesomeProject/datarace/main.go: +0x7b
  16.  
  17. Goroutine (running) created at:
  18.  
  19. main.main()
  20.  
  21. /Users/saas/src/awesomeProject/datarace/main.go: +0x4f
  22.  
  23. ==================

那要怎么改良这个程序呢?改法很简单,也有很多种。上面我们已经提到了Golang并发的口号是:不要通过共享内存来通信,而是通过通信来共享内存。先来看下通过共享内存来通信的改良版:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "runtime"
  6. "sync"
  7. "time"
  8. )
  9.  
  10. var i int64 =
  11.  
  12. func main() {
  13. runtime.GOMAXPROCS()
  14. var m sync.Mutex
  15. go func() {
  16. for {
  17. m.Lock()
  18. fmt.Println("i is", i)
  19. m.Unlock()
  20. time.Sleep(time.Second)
  21. }
  22. }()
  23.  
  24. for {
  25. m.Lock()
  26. i +=
  27. m.Unlock()
  28. }
  29. }

通过加锁互斥访问(共享)变量i,也就是上面所说的通过共享内存来通信。那么通过通信来共享内存也是怎么实施的呢?答案是用channel:

  1. package main
  2.  
  3. import (
  4. "fmt"
  5. "runtime"
  6. "time"
  7. )
  8.  
  9. var i int64 =
  10.  
  11. func main() {
  12. runtime.GOMAXPROCS()
  13. c := make(chan int64)
  14. go func() {
  15. for {
  16. fmt.Println("i is", <-c)
  17. time.Sleep(time.Second)
  18. }
  19. }()
  20.  
  21. for {
  22. i +=
  23. c<-i
  24. }
  25. }

上面提到了一些channel的缺点,文章一开始我也提到了channel能解决Golang并发编程的90%问题,那剩下的一些少数并发情况用什么更优的方案呢?

锁会不会是个更优的解决方案呢?

锁就像厕所的坑位一样,你占用的时间越长,等待的人排的队就会越长。读写锁只会减缓这种情况。另外使用多个锁很容易导致死锁。总而言之,锁不是我们只在寻找的方案。

原子操作

原子操作是这10%场景有限考虑的解决方案。原子操作是在CPU层面保证了原子性。不用编程人员加锁。Golang对应的操作在sync.atomic 包。Store, Load,  Add, Swap 和 CompareAndSwap方法。

CompareAndSwap 方法

  1. type Spinlock struct {
  2.  
  3.   state *int32
  4.  
  5. }
  6.  
  7. const free = int32()
  8.  
  9. func (l *Spinlock) Lock() {
  10.  
  11.   for !atomic.CompareAndSwapInt32(l.state, free, ) { //如果state等于0就赋值为42
  12.  
  13.     runtime.Gosched() //让出CPU
  14.  
  15.   }
  16.  
  17. }
  18.  
  19. func (l *Spinlock) Unlock(){
  20.  
  21.   atomic.StoreInt32(l.state, free) // 所有操作state变量的操作都应该是原子的
  22.  
  23. }

基于上面的一些并发实践的建议是:

1.避免阻塞,避免数据竞态

2.用channel避免共享内存,用select管理channel

3.当channel不适用于你的场景时,尽量用sync包的原子操作,如果实在需要用到锁,尽量缩小锁的粒度(锁住尽量少的代码)。

并发程序找错

根据前面介绍的内容,我们来看看下面的这个例子有没有什么问题:

  1. func restore(repos []string) error {
  2. errChan := make(chan error, )
  3. sem := make(chan int, ) // four jobs at once
  4. var wg sync.WaitGroup
  5. wg.Add(len(repos))
  6. for _, repo := range repos {
  7. sem <-
  8. go func() {
  9. defer func() {
  10. wg.Done()
  11. <- sem
  12. }()
  13. if err := fetch(repo); err != nil {
  14. errChan <- err
  15. }
  16. }()
  17. }
  18. wg.Wait()
  19. close(sem)
  20. close(errChan)
  21. return <- errChan
  22. }

Bug1. sem无需关闭

Bug2.go和匿名函数触发的bug,repo不断在更新,fetch拿到的repo是未定义的。有data race问题。

Bug3.sem<-1放在go func外面启动同时有4个goroutine在运行,并不能很好的控制同时有4个fetch任务。

Bug4. errChan的缓冲区大小为1,当多个fetch产生err时,将会导致程序死锁。

改良后的程序:

  1. func restore(repos []string) error {
  2. errChan := make(chan error, )
  3. sem := make(chan int, ) // four jobs at once
  4. var wg sync.WaitGroup
  5. wg.Add(len(repos))
  6. for _, repo := range repos {
  7. go worker(repo, sem, &wg, errChan)
  8. }
  9. wg.Wait()
  10. close(errChan)
  11. return <- errChan
  12. }
  13.  
  14. Func worker(repo string, sem chan int, wg *sync.WaitGroup, errChan chan err) {
  15. defer wg.Done()
  16. sem <-
  17. if err := fetch(repo); err != nil {
  18. select {
  19. case errChan <- err:
  20. // we are the first worker to fail
  21. default:
  22. // some other failure has already happened, drop this one
  23. }
  24. }
  25. <- sem
  26. }

最后思考:为什么errChan一定要close?

因为最后的return<-errChan,如果fetch的err都为nil,那么errChan就是空,<-errChan是个永久阻塞的操作,close(sem)会触发读事件,返回chan累心的零值,这里是nil。

基于上面的一些并发实践的建议是:

1.channel不是socket和file这种资源,不需要通过close来释放资源

2.避免将goroutine和匿名函数一起使用

3.在你启动一个goroutine之前,一定要清楚它会在什么时候,什么情况下会退出。

总结

本文介绍了Golang并发编程的一些高效实践建议,旨在让大家在Golang并发实践中少踩坑。其中data race问题和goroutine退出的时机尤为重要。

参考

https://www.youtube.com/watch?v=YEKjSzIwAdA

https://www.youtube.com/watch?v=yKQOunhhf4A

https://www.youtube.com/watch?v=QDDwwePbDtw

https://ms2008.github.io/2019/05/12/golang-data-race/

Golang 高效实践之并发实践的更多相关文章

  1. Golang 高效实践之并发实践context篇

    前言 在上篇Golang高效实践之并发实践channel篇中我给大家介绍了Golang并发模型,详细的介绍了channel的用法,和用select管理channel.比如说我们可以用channel来控 ...

  2. Golang高效实践之泛谈篇

    前言 我博客之前的Golang高效实践系列博客中已经系统的介绍了Golang的一些高效实践建议,例如: <Golang高效实践之interface.reflection.json实践>&l ...

  3. Struts2、Spring、Hibernate 高效开发的最佳实践(转载)

    Struts2.Spring.Hibernate 高效开发的最佳实践 Struts2.Spring.Hibernate(SSH)是最常用的 Java EE Web 组件层的开发技术搭配,网络中和许多 ...

  4. 心知天气数据API 产品的高并发实践

    心知天气数据API 产品的高并发实践 心知天气作为国内领先的商业气象服务提供商,天气数据API 产品从公司创立以来就一直扮演着很重要的角色.2009 年API 产品初次上线,历经十年,我们不断用心迭代 ...

  5. Golang在京东列表页实践总结

    Golang在京东列表页实践总结 作者:张洪涛 10余年软件开发和设计经验,曾就职于搜狐.搜狗.前matrixjoy公司联合创始人.甘普科技CTO. 目前线上状态 基于搜索实现: 全量数据,搜索结果不 ...

  6. 【Scala】Scala多线程-并发实践

    Scala多线程-并发实践 scala extends Thread_百度搜索 scala多线程 - 且穷且独立 - 博客园 Scala和并发编程 - Andy Tech Talk - ITeye博客 ...

  7. 《python编程从入门到实践》读书实践笔记(二)

    本文是<python编程从入门到实践>读书实践笔记11章的内容,主要包含测试,为体现测试的重要性,独立成文. 11 测试代码 写在前面的话,以下是我这些年开发中和测试相关的血泪史. 对于一 ...

  8. 《python编程从入门到实践》读书实践笔记(一)

    本文是<python编程从入门到实践>读书实践笔记1~10章的内容,主要包含安装.基础类型.函数.类.文件读写及异常的内容. 1 起步 1.1 搭建环境 1.1.1 Python 版本选择 ...

  9. Golang 高效实践之defer、panic、recover实践

    前言 我们知道Golang处理异常是用error返回的方式,然后调用方根据error的值走不同的处理逻辑.但是,如果程序触发其他的严重异常,比如说数组越界,程序就要直接崩溃.Golang有没有一种异常 ...

随机推荐

  1. 给Delphi程序添加版本信息(EXE和Dll)

    我们在用Delphi编译完程序,准备发布产品时,总希望随产品发布个性信息以标示产品的来源以及开发者等信息,就像windows的程序一样,使我们一看属性就知道他是微软的产品,这些在Delphi中是如何实 ...

  2. 零元学Expression Blend 4 - Chapter 44 Flash做的到的Blend也可以!轻松制作拥有动画的MenuBar!(下)

    原文:零元学Expression Blend 4 - Chapter 44 Flash做的到的Blend也可以!轻松制作拥有动画的MenuBar!(下) 抱歉久等了!!!! 终於到了动画MenuBar ...

  3. 数据库连接池之_c3p0

    C3p0 1,手动设置参数 @Test public void demo1(){ Connection connection =null; PreparedStatement preparedStat ...

  4. MinGW 编译 libsndfile-1.0.25(只要有 MSYS,./configure make make install 就行了)

    最近做的一个项目需要读写 wav 文件.在网上找到 libsndfile 刚好满足我的需要.但是编译的时候遇到了点小麻烦,这里记录一下编译的过程,免得下次再编译时忘记了. 因为是在编译完成若干天后写的 ...

  5. baiduMap试手《办理进京证和市区警察查询进京证的地址浏览》

    没用过baidu的map api其实挺简单,申请一个key,然后根据坐标在地图上生成对象,看了官方的dome多少知道有些什么功能了,没什么可说的直接贴效果. <!DOCTYPE html> ...

  6. 【Linux】Linux 环境下splite以及一些正则使用

    由于在windows下,遍历目录,想查找满足条件的文件: dir /s > ..\fileresult.txt 结果得到的文件过大,999多MB的txt: split -b 10k date.f ...

  7. Codility---BinaryGap

    Task description A binary gap within a positive integer N is any maximal sequence of consecutive zer ...

  8. 什么是T1与E1线路

    Leased Line 租用线路 租用线路是电信公司为某一机构建造的永久性通信电路.租用线路旁路了本地交换电信局(LEC)上的交换设备,所以在每次数据传输之前无需起始阶段,它们总是连通的.如果线路是长 ...

  9. 快速删除mysql表中的数据

    一.清空全部数据,不写日志,不可恢复,速度很快 truncate table 表名; 二.清空全部数据,写日志,可恢复,速度很慢 delete from 表名;

  10. 微服务SpringCloud之服务注册与发现

    在找.net core 微服务框架时发现了Steeltoe开源项目,它可以基于Spring Cloud实现.net core和.net  Framework的微服务.正好之前也有学习过SpringBo ...