Golang 高效实践之并发实践

前言

在我前面一篇文章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 <- 1

// 3) 同步

go func() { <-unbuffered }()

unbuffered <- 1

带buffer的channel阻塞情况

buffered := make(chan int, 1)

// 4) 阻塞

a := <- buffered

// 5) 不阻塞

buffered <-1

// 6) buffer满,阻塞

buffered <-2

上述情况其实归纳起来很简单:不管有无缓冲区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 0, 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 0, 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 = 0

func main() {
    runtime.GOMAXPROCS(2)
    go func() {
        for {
            fmt.Println("i is", i)
            time.Sleep(time.Second)
        }
    }()

    for {
        i += 1
    }
}

在我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 6:

  main.main.func1()

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

Previous write at 0x00000121e848 by main goroutine:

  main.main()

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

Goroutine 6 (running) created at:

  main.main()

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

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

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

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

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

原子操作

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

CompareAndSwap 方法

type Spinlock struct {

  state *int32

}

const free = int32(0)

func (l *Spinlock) Lock() {

  for !atomic.CompareAndSwapInt32(l.state, free, 42) { //如果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, 1)
    sem := make(chan int, 4) // four jobs at once
    var wg sync.WaitGroup
    wg.Add(len(repos))
    for _, repo := range repos {
        sem <- 1
        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, 1)
    sem := make(chan int, 4) // 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 <- 1
    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/

原文地址:https://www.cnblogs.com/makelu/p/11205704.html

时间: 2024-11-02 03:57:18

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

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

前言 在上篇Golang高效实践之并发实践channel篇中我给大家介绍了Golang并发模型,详细的介绍了channel的用法,和用select管理channel.比如说我们可以用channel来控制几个goroutine的同步和退出时机,但是我们需要close channel通知其他接受者,当通知和通信的内容混在一起时往往比较复杂,需要把握好channel的读写时机,以及不能往已经关闭的channel中再写入数据.如果有没有一种更好的上下文控制机制呢?答案就是文章今天要介绍的context,

Python数据分析基础与实践 Python数据分析实践课程 Python视频教程

课程简介: Python数据分析基础与实践 Python数据分析实践课程 Python视频教程----------------------课程目录------------------------------├<章节1Python概况>├<章节2Python安装>├<章节3数据准备>├<章节4数据处理>├<章节5数据可视化>├<章节6网页数据抓取>├<章节7连接MySQL>├<章节8数据分析> 下载地址:百度网盘

golang的多协程实践

go语言以优异的并发特性而闻名,刚好手上有个小项目比较适合. 项目背景: 公司播控平台的数据存储包括MySQL和ElasticSearch(ES)两个部分,编辑.运营的数据首先保存在MySQL中,为了实现模糊搜索和产品关联推荐,特别增加了ES,ES中保存的是节目集的基本信息. 本项目是为了防止实时同步数据出现问题或者系统重新初始化时的全量数据同步而做的.项目主要是从MySQL读取所有的节目集数据写入到ES中. 项目特点: 因为节目集数量较大,不能一次性的读入内存,因此每次读出一部分记录写入ES.

高效运维最佳实践七字诀,不再憋屈的运维!

我们那么努力,为什么总感觉过得那么憋屈.苦闷?做的事情那么多,为什么业务部门.直接领导和公司貌似都那么不领情?怎么做才能自己更加开心些? 做运维的那么多,快乐的能有几个? 我们那么努力,为什么总感觉过得那么憋屈.苦闷?做的事情那么多,为什么业务部门.直接领导和公司貌似都那么不领情?怎么做才能自己更加开心些? 本专栏的主线实际是一个运维人员的十年成长史,从菜鸟到运维总监.但不是基础技术教学,也不会在运维技术的某一方面过深涉及.更多的是应用技巧.实践经验及案例剖析.专栏中的系列文章,包含作者在运维各

Python并发实践_02_协程

python中实现并发的方式有很多种,通过多进程并发可以真正利用多核资源,而多线程并发则实现了进程内资源的共享,然而Python中由于GIL的存在,多线程是没有办法真正实现多核资源的. 对于计算密集型程序,应该使用多进程并发充分利用多核资源,而在IO密集型程序中,多核优势并不明显,甚至由于大多数时间都是在IO堵塞状态,多进程的切换消耗反而让程序效率更加低下. 而当需要并发处理IO密集型任务时,就需要用到协程(Coroutine).协程并没有系统级的调度,而是用户级的调度方式,避免了系统调用的开销

Linux服务器高并发实践经历

作为一个师父离职早的野生程序员,业务方面还可以达到忽悠别人的水平,但上升到性能层面那就是硬伤. 真实天上掉馅饼,公司分配了一个测试性能的任务,真是感觉我的天空星星都亮了. 高并发主要限制因素:CPU.网络流量.内存.系统配置 CPU 用top看cpu利用率,按1查看每个cpu线程的工作情况:这里面会显示出cpu的空闲.利用率.软中断等状态 如果某个cpu线程使用率经常达到100%,那cpu就成了瓶颈,通常为了实现高并发,负载比较大的服务程序会自己绑定cpu,使自己的任务分配到多个cpu线程中去,

【GoLang】GoLang GOPATH 工程管理 最佳实践

参考资料: MAC下 Intellij IDEA GO语言插件安装及简单案例:http://blog.csdn.net/fenglailea/article/details/53054502 关于win下intellij idea 与gopath的问题:http://www.golangtc.com/t/55053967421aa9386f0000b1 [翻译]十条有用的 GO 技术:http://mikespook.com/2014/07/%E5%8D%81%E6%9D%A1%E6%9C%89

笔记:java并发实践2

public interface Executor { void execute(Runnable command); } 虽然Executor是一个简单的接口,但它为灵活且强大的异步任务框架提供了基础,该框架能支持多种不同类型的任务执行策略.它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runable来表示任务.Executor的实现还提供了对生命周期的支持,以及统计信息收集,应用程序管理机制和性能检测等机制. Executor is based on the producer

golang 碎片整理之 并发

并发与并行 并发:同一时间段内执行多个任务.并行:同一时刻执行多个任务. Go语言的并发通过goroutine实现.goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作.goroutine是由go语言的运行调度完成的,而线程是由操作系统调度完成的.Go语言还提供了channel在多个goroutine间进行通信.goroutine和channel是go语言秉承的CSP(Communicating Sequential Process)并发模式的