前言

在我前面一篇文章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阻塞情况

unbuffered := make(chan int)

a := <- unbuffered // 阻塞

unbuffered  := make(chan int) 

// 1) 阻塞

a := <- unbuffered

// 2) 阻塞

unbuffered <- 

// 3) 同步

go func() { <-unbuffered }()

unbuffered <- 

带buffer的channel阻塞情况

buffered := make(chan int, )

// 4) 阻塞

a := <- buffered

// 5) 不阻塞

buffered <-

// 6) buffer满,阻塞

buffered <-

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

不带buffer和带buffer的channel用途:

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

关闭channel

c := make(chan int)

close(c)

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不阻塞

func TryReceive(c <-chan int) (data int, more, ok bool) {

  select {

  case data, more = <- c:

    return data, more, true

  }

  default:

    return , true, false

}

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

或者超时返回:

func TryReceiveWithTimeout(c <-chan int, duration time.Duration) (data int, more, ok bool) {

  select {

  case data, more = <-c:

    return data, more, true

  case <- time.After(duration):

    return , true, false
  }
}

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同时读写一个变量,这种行为是未定义的,也就是说读变量出来的值很有可能不是写入的值,这个值是任意值都有可能。

例如下面这段代码:

package main

import (
"fmt"
"runtime"
"time"
) var i int64 = func main() {
runtime.GOMAXPROCS()
go func() {
for {
fmt.Println("i is", i)
time.Sleep(time.Second)
}
}() for {
i +=
}
}

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

==================

WARNING: DATA RACE

Read at 0x00000121e848 by goroutine :

  main.main.func1()

      /Users/saas/src/awesomeProject/datarace/main.go: +0x3e

Previous write at 0x00000121e848 by main goroutine:

  main.main()

      /Users/saas/src/awesomeProject/datarace/main.go: +0x7b

Goroutine  (running) created at:

  main.main()

      /Users/saas/src/awesomeProject/datarace/main.go: +0x4f

==================

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

package main

import (
"fmt"
"runtime"
"sync"
"time"
) var i int64 = func main() {
runtime.GOMAXPROCS()
var m sync.Mutex
go func() {
for {
m.Lock()
fmt.Println("i is", i)
m.Unlock()
time.Sleep(time.Second)
}
}() for {
m.Lock()
i +=
m.Unlock()
}
}

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

package main

import (
"fmt"
"runtime"
"time"
) var i int64 = func main() {
runtime.GOMAXPROCS()
c := make(chan int64)
go func() {
for {
fmt.Println("i is", <-c)
time.Sleep(time.Second)
}
}() for {
i +=
c<-i
}
}

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

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

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

原子操作

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

CompareAndSwap 方法

type Spinlock struct {

  state *int32

}

const free = int32()

func (l *Spinlock) Lock() {

  for !atomic.CompareAndSwapInt32(l.state, free, ) { //如果state等于0就赋值为42

    runtime.Gosched() //让出CPU

  }

}

func (l *Spinlock) Unlock(){

  atomic.StoreInt32(l.state, free)  // 所有操作state变量的操作都应该是原子的

}

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

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

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

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

并发程序找错

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

func restore(repos []string) error {
errChan := make(chan error, )
sem := make(chan int, ) // four jobs at once
var wg sync.WaitGroup
wg.Add(len(repos))
for _, repo := range repos {
sem <-
go func() {
defer func() {
wg.Done()
<- sem
}()
if err := fetch(repo); err != nil {
errChan <- err
}
}()
}
wg.Wait()
close(sem)
close(errChan)
return <- errChan
}

Bug1. sem无需关闭

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

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

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

改良后的程序:

func restore(repos []string) error {
errChan := make(chan error, )
sem := make(chan int, ) // four jobs at once
var wg sync.WaitGroup
wg.Add(len(repos))
for _, repo := range repos {
go worker(repo, sem, &wg, errChan)
}
wg.Wait()
close(errChan)
return <- errChan
} Func worker(repo string, sem chan int, wg *sync.WaitGroup, errChan chan err) {
defer wg.Done()
sem <-
if err := fetch(repo); err != nil {
select {
case errChan <- err:
// we are the first worker to fail
default:
// some other failure has already happened, drop this one
}
}
<- sem
}

最后思考:为什么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. SQLDirect 6.5 Source (Delphi 5-10.1 Berlin)

    Description:SQLDirect Component Library is a light-weight Borland Database Engine replacement for Bo ...

  2. 从wireshark中学网络分析(一)

    http://blog.csdn.net/nk_test/article/details/56509688 http://blog.csdn.net/nk_test/article/details/5 ...

  3. Delphi用Socket API实现路由追踪

    Windows自带的Tracert是向远程主机发送ICMP包进行追踪,但是目前很多主机关闭了ICMP答复,这个工具不太好使了~~~~~原理咱知道,正规的Trace不就是发送TTL依次递增的UDP包吗? ...

  4. qobject_cast<QPushButton*>(sender()) 简化信号与槽的编写(sender()取得发信号的对象后,就取得了它的全部信息,为所欲为)

    当你觉得写代码是一件重复性极高的工作时,这时你就应该考虑换个方式来实现了. 提高代码效率,减少代码量. 代码片: void Widget::onClicked() { QPushButton* but ...

  5. 【练习题】proj2 字符串压缩

    输入一个字符串,输出简单的压缩 1)单字符串压缩 : 输入:ABBBCCD , 输出AB3C2D 2)多字符串压缩 输入:AABCABCD,输出A(ABC)2D 1)压缩单个字符 #include & ...

  6. java的equals与==的区别

    看了网上关于equal与==的区别,感觉很多有些片面,不仔细,这里我来说说我对equal与==的理解 首先要了解基本类型与引用类型 1.int,char,boolean之类的就是基本类型,我们只要使用 ...

  7. HBase —— 集群环境搭建

    一.集群规划 这里搭建一个3节点的HBase集群,其中三台主机上均为Regin Server.同时为了保证高可用,除了在hadoop001上部署主Master服务外,还在hadoop002上部署备用的 ...

  8. 曹工说Tomcat3:深入理解 Tomcat Digester

    一.前言 我写博客主要靠自己实战,理论知识不是很强,要全面介绍Tomcat Digester,还是需要一定的理论功底.翻阅了一些介绍 Digester 的书籍.博客,发现不是很系统,最后发现还是官方文 ...

  9. hexo + next搭建博客

    使用hexo和next主题搭建GitHub博客 安装node.js 在node.js 官网下载, 相应系统位数的软件包. 可以选择LTS长期支持版, 或者Current当前版本. 我这里选择的是Win ...

  10. RDS数据库磁盘满导致实例锁定

    问题描述: 阿里云RDS空间不足,进行报警.收到报警后.对数据库中不重要的数据备份后执行delete删除操作.执行成功后发现数据删掉了.但是数据库的空间并没有释放.数据占用空间反而越来越大,最后RDS ...