前言

在我前面一篇文章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. TIFF图片简介

    每个TIFF文件都是从指示字节顺序的两个字节开始的.“II”表示小字节在先.“MM”表示大字节在先字节顺序.后面的两个字节表示数字42.数字42是“为了其深刻的哲学意义"而选择的. 42的读 ...

  2. Android零基础入门第52节:自定义酷炫进度条

    原文:Android零基础入门第52节:自定义酷炫进度条 Android系统默认的ProgressBar往往都不能满足实际开发需要,一般都会开发者自定义ProgressBar. 在Android开发中 ...

  3. 了解Activity

    Android中的activity全都归属于task管理 .task 是多个 activity 的集合,这些 activity 按照启动顺序排队存入一个栈(即“back stack”).android ...

  4. 使用Visual Studio开发Python

    Python优秀的集成开发环境有PyCharm,Visual Studio Code等,当然你仍然可以使用Visual Studio进行开发.如果你熟悉Visual Studio,使用Visual S ...

  5. 使用Boost的DLL库管理动态链接库

    Boost 1.61新增了一个DLL库,跟Qt中的QLibrary类似,提供了跨平台的动态库链接库加载.调用等功能.http://www.boost.org/users/history/version ...

  6. Qt 5.3更新无数,更改C++控制台输出最为赞(这样就和普通C++ IDE没区别了)

    转载请注明文章:Qt 5.3更新无数,更改C++控制台输出最为赞 出处:多客博图 本人觉得有了这个更新,Qt Creator可谓几乎没有缺点了,起码仅仅开发C/C++,是不用再去安装VS了. Qt 5 ...

  7. 剖析Qt的事件机制原理(源代码级别)

    在用Qt写Gui程序的时候,在main函数里面最后依据都是app.exec();很多书上对这句的解释是,使Qt程序进入消息循环.下面我们就到exec()函数内部,来看一下他的实现原理.Let's go ...

  8. Codility------CyclicRotation

    Task description A zero-indexed array A consisting of N integers is given. Rotation of the array mea ...

  9. ASP.NET Web API 直到我膝盖中了一箭【1】基础篇

    蓦然回首,那些年,我竟然一直很二. 小时候,读武侠小说的时候,看到那些猪脚,常常会产生一种代入感,幻想自己也会遭遇某种奇遇,遇到悬崖跳下去是不是有本“武林秘笈”在等着?长大以后也是一样,多少人梦着醒着 ...

  10. python下SQLAlchemy的使用

    SQLAlchemy是python中orm常用的框架.支持各种主流的数据库,如SQLite.MySQL.Postgres.Oracle.MS-SQL.SQLServer 和 Firebird. 在安装 ...