关于Golong中的channel

引入

Golang的一个特别重要的特性就是简化了对多任务执行的操作,使用goroutine这样一个轻量级的协程来执行异步操作。当涉及到多任务的时候,不可避免地就会想到对共享资源的读写一致性问题。比如当一个goroutine在对一个map进行写入操作的时候,另一个goroutine也在做写入操作,那么如果对于写入的顺序有要求,那么就需要有一个机制来保证这种顺序。

在Golang中,对于同步的解决方案有两种

  • 锁:sync.Mutex或者sync.RWMutex
  • 管道:chan

如果多个goroutine中间没有数据传输,那么可以使用锁(sync.Mutext)来实现对共享数据操作的一致性要求。

比如下面的这个例子,使用15个协程来分别计算每个数的阶乘,让后将这个数作为key,将阶乘作为value放入到一个map中,这个map就是一个共享资源。每个协程都在向其中写入数据,假如没有加锁,那么在写入的时候,就会报错:fatal error: concurrent map writes

var (
  lock sync.Mutex
  dict = make(map[int]int, 10)
)

func Factorial(num int) {
  for i := 1; i <= num; i++ {
    go factorialOne(i)
  }
}

func factorialOne(num int) {
  res := 1
  for i := 1; i <= num; i++ {
    res *= i
  }
  lock.Lock() // 加锁
  dict[num] = res
  lock.Unlock() // 解锁
}

func main() {

  Factorial(15)

  // 主线程等待10秒
  time.Sleep(10 * time.Second)

  lock.Lock() // 加锁
  for key, value := range dict {
    fmt.Printf("%v! = %v\n", key, value)
  }
  lock.Unlock() // 解锁

}

管道(chan)

当多个goroutine之间需要共享数据的同时,还需要传输数据,那么管道(chan)是一个最佳的选择。

我们可以通过make函数创建一个指定类型的chan,比如下面的例子中,我们创建了一个int类型的chan

ch1 := make(chan int)
ch2 := make(chan int, 10)

我们可以大致地认为chan有两种,一种是有缓存的(buffered),一种是没有缓存的。上面例子中的ch1是没有缓存的,而ch2是有缓存的。我们在使用管道的时候,需要注意这两种管道的区别。以下摘自channels

Receivers always block until there is data to receive. If the channel is unbuffered, the sender blocks until the receiver has received the value. If the channel has a buffer, the sender blocks only until the value has been copied to the buffer; if the buffer is full, this means waiting until some receiver has retrieved a value.

我简单地做了一个总结如下:

  • 读总是会阻塞,直到有数据可读
  • 执行顺序
    • 没有缓存的(unbuffered):先读后写
    • 有缓存的(buffered)
      • 管道未满:先写后读
      • 管道已满:先读后写

注:读就是receive,比如<-ch,写就是send,比如ch <- 0

所谓的先A后B,指的是,当程序执行到B的时候,需要阻塞,直到A执行完毕。这样就能保证A和B的执行顺序。

比如下面的例子(摘自https://golang.org/ref/mem](https://golang.org/ref/mem

var c = make(chan int)
var a string

func f() {
  a = "hello, world"
  <-c
}
func main() {
  go f()
  c <- 0
  print(a)
}

在上面的例子中,hello, world肯定会打印出来。因为管道c是unbuffered,所以遵守先读后写的规则,即程序会在写(c <- 0)的时候阻塞,直到读(<-c)操作结束。即:main()函数中的写操作(c <- 0)会先于f()函数的读操作(<-c)。所以在执行print(a)的时候,f()已经执行完毕,肯定会输出hello, world

但是,如果上面例子中的管道是缓存的(buffered),那么hello, world就不会肯定输出。因为这时的管道还没有满,遵守先写后读的规则。即main()函数中的写(c <- 0)先执行,函数f()中的读(<-c)后执行。即:在main()函数中会不会有阻塞等待发生,但是在f()中却有,所以当执行到print(a)的时候,并不能保证a已经被赋值。

var c = make(chan int, 2) // 创建一个有buffer的管道
var a string

func f() {
  a = "hello, world"
  <-c
}
func main() {
  go f()
  c <- 0
  print(a)
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。