首页 > 其他分享 >golang并发channel实践

golang并发channel实践

时间:2023-06-07 17:44:12浏览次数:43  
标签:Golang golang 并发 func go main channel

前言

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

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

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

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

var i int64 = 0

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

    for {
        m.Lock()
        i += 1
        m.Unlock()
    }
}

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

package main

import (
    "fmt"
    "runtime"
    "time"
)

var i int64 = 0

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

    for {
        i += 1
        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(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.toutiao.com/article/7241238450241045051/?log_from=05adb213f3c58_1686129610855

标签:Golang,golang,并发,func,go,main,channel
From: https://www.cnblogs.com/nizuimeiabc1/p/17464098.html

相关文章

  • Golang中的panic
    Golang中的panic引言在软件开发过程中,出现错误是很常见的。在Golang中,当程序发生无法处理的错误时,它会引发panic。panic是一种异常情况,它会导致程序终止并显示错误消息。虽然panic在某些情况下是必要的,但它可能会对程序的性能和可靠性产生负面影响。在本文中,我们将深入探讨Golang......
  • 其他细节:并发并行
        ......
  • 接口并发能力优化提升
    写了一个插入接口,进行了并发处理的优化,优化过程如下:初始接口代码yml配置文件server:port:8081spring:datasource:url:jdbc:mysql://ip:3306/my_test?characterEncoding=utf-8&useSSL=false&serverTimezone=UTCusername:linpassword:......
  • Golang中如何控制goroutine的执行顺序?
    首先说明一下原理:前后协程之间通过通道去相互限制,后一个线程尝试去获取一个channel的值,当channel中没有值时,就会一直阻塞,而前一个协程则负责关闭channel,当前一个协程完成了这个操作,后一个协程才可以结束阻塞,继续执行。示例代码:packagemainimport( "fmt" "time")funcma......
  • 高并发---限流
    在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统能处理的容量,可谓是抗高并发流量的银弹;而降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺......
  • 算法 in Golang:Quicksort(快速排序)
    算法inGolang:Quicksort(快速排序)Quicksort(快速排序)快速排序O(nlog2^n),比选择排序要快O(n²)在日常生活中经常使用使用了D&C策略(分而治之)使用Quicksort排序数组不需要排序的数组(也就是BaseCase基线条件):[],空数组[s],单元素数组很容易排序的数组:[a,b],两......
  • mysql使用efcore实现乐观并发控制
    为了避免多个用户同时操作同一个资源造成的并发冲突问题,通常需要进行并发控制。并发控制分为:乐观和悲观两策略悲观:悲观并发控制一般采用行锁、表锁等排它销对资源进行锁定,确保一个时间点只有一个用户在操作被锁定的资源。 悲观并发控件的使用比较简单,仅对要进行并发控制的资......
  • 算法 in Golang:Recursion(递归)
    算法inGolang:Recursion(递归)递归算法场景:在套娃中找到宝石可以这样做while没找到:if当前项is宝石:return宝石elseif当前项is套娃:打开这个套娃if当前项is宝石:return宝石elseif当前项is套娃:打开这个套娃if当前项is宝石:............
  • 20个Golang片段让我不再健忘
    前言本文使用代码片段的形式来解释在 go 语言开发中经常遇到的小功能点,由于本人主要使用 java 开发,因此会与其作比较,希望对大家有所帮助。1.helloworld新手村的第一课,毋庸置疑。packagemainimport"fmt"funcmain(){ fmt.Printf("helloworld")}2.隐形初始......
  • golang中for select时,如果channel关闭会怎么样?
    首先,如果对于一个已经关闭的channel来说,如果此时channel里还有值,则会正确读到channel里的值,且返回的第二个bool值为true;如果关闭前,channel里的值已经被读完,则最后返回的则是channel的零值;那么针对该问题,我们通过代码来验证一下:packagemainimport( "fmt" "time")constt......