【Go并发编程】channel

135 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情

channel

channel是go语言基于CSP模型创建的gorouting间的通讯机制。它可以实现:

  • 数据交流:当作并发的 buffer 或者 queue,解决生产者 - 消费者问题。多个 goroutine 可以并发当作生产者(Producer)和消费者(Consumer)。
  • 数据传递:一个 goroutine 将数据交给另一个 goroutine,相当于把数据的拥有权 (引用) 托付出去。
  • 信号通知:一个 goroutine 可以将信号 (closing、closed、data ready 等) 传递给另一个或者另一组 goroutine 。
  • 任务编排:可以让一组 goroutine 按照一定的顺序并发或者串行的执行,这就是编排的功能。
  • 锁:利用 Channel 也可以实现互斥锁的机制。

CSP

CSP(Communicating Sequential Processes)通信顺序进程

不要以共享内存的方式来通信,相反,要通过通信来共享内存

go语言实现主要包含channel和gorouting,channel实现gorouting间的通信

channel的基本使用

channel和Slice、map一样需要make开炮空间才能使用。

var ch1 chan int
ch1 = make(chan int)
ch2 = make(chan int,5)
go func() {
   ch1 <- 3 // 向channel发送数据
}()
fmt.Println(<-ch1)  // 接收数据
close(ch1) //关闭

channel种类:

  • 无缓冲channel:没有缓冲区,收发者同时在场才能通信
  • 有缓冲channel:有一定容量的缓冲区,更加灵活自由,不会直接阻塞
  • send-only:只发送
  • recv-only:只接收

只发送 channel 类型和只接收 channel 类型,会被用作函数的参数类型或返回值,用于限制对 channel 内的操作,或者是明确可对 channel 进行的操作的类型

func produce(ch chan<- int) {
   for i := 0; i < 10; i++ {
      ch <- i + 1
      time.Sleep(time.Second)
   }
   close(ch)
}

func consume(ch <-chan int) {
   for n := range ch {
      println(n)
   }
}

ch := make(chan int, 5)
go produce(ch)
go consume(ch)

channel的数据接收与关闭:

  • 通常channel在发送方进行关闭
  • n := <-ch1,当ch被关闭后,n将被赋值为ch元素类型的零值
  • n,ok := <-ch1,当ch被关闭后,m将被赋值为ch元素类型的零值, ok值为false
  • for v := range ch,当ch被关闭后,for range循环结束
  • select:同时对多个 channel 进行操作
select {
case x := <-ch1:
   fmt.Println("get ch1:", x)
case y := <-ch1:
   fmt.Println("get ch2:", y)
case <-time.After(time.Second * 3):
   fmt.Println("out time")
}

对于channel的异常场景,通过下面的表格进行说明:

image.png

channel使用场景

无缓冲 channel

信号传递

无缓冲的channel在接收时如果没有数据会先阻塞,利用这特性,可以实现一对一的信号的传递:

type signal struct{}

func work(c chan signal) {
   println("worker is working...")
   time.Sleep(1 * time.Second)
   c <- signal{}
}

func main() {
   println("start a worker...")
   c := make(chan signal)
   go work(c)
   <-c
   fmt.Println("worker work done!")
}

当然也可以实现一对多的信号传递:利用channel在close时,<-ch会收到一个默认值。

type signal struct{}

var wg sync.WaitGroup

func main() {
   fmt.Println("start a group of workers...")
   groupSignal := make(chan signal)
   wg.Add(5)
   for i := 0; i < 5; i++ {
      go func(i int) {
         <-groupSignal
         fmt.Printf("worker %d: start to work...\n", i)
         wg.Done()
      }(i + 1)
   }
   fmt.Println("the group of workers start to work...")
   close(groupSignal)
   wg.Wait()
   fmt.Println("the group of workers work done!")
}

替代锁机制

可以利用无缓冲channel的阻塞性质实现临界区的功能,开启一个gorouting接收加锁信号,当收到信号,就可以运行下面的工作,运行完成再把消息返回给调用端,继续执行后面的代码:

type counter struct {
	c1 chan int
	c2 chan int
	i  int
}

func main() {
    cter := &counter{
        c1: make(chan int),
        c2: make(chan int),
    }
    go func() {
        for {
            cter.c1 <- 1
            // 加锁的操作
            cter.i++
            cter.c2 <- 1
        }
    }()
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            // 开始加锁
            <-cter.c1
            // 运行完成
            <-cter.c2
            fmt.Println(cter.i)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

带缓冲 channel

消息队列

channel 的原生特性与消息队列十分相似,包括 Goroutine 安全、有 FIFO(first-in, first out)保证等。

  • 无论是 1 收 1 发还是多收多发,带缓冲 channel 的收发性能都要好于无缓冲 channel;
  • 对于带缓冲 channel 而言,发送与接收的 Goroutine 数量越多,收发性能会有所下降;
  • 对于带缓冲 channel 而言,选择适当容量会在一定程度上提升收发性能。

计数信号量

固定容量的channel相当于信号量的大小,请求信号量相当于向channel发送消息,缓冲满了会阻塞,接收channel消息相当于释放信号量。

func main() {
    active := make(chan struct{}, 3)
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(j int) {
            active <- struct{}{}
            fmt.Printf("handle job: %d\n", j)
            time.Sleep(2 * time.Second)
            <-active
            wg.Done()
        }(i)
    }

    wg.Wait()
}

nil channel

channel 类型变量的值为 nil,读写都会发生阻塞。利用这个特定,当我们关闭channel后,它仍然可以就收空值,影响select的判断,我们可以将关闭的channel赋值为空,这样子就直接阻塞掉了。

ch1, ch2 := make(chan int), make(chan int)
go func() {
   time.Sleep(time.Second * 5)
   ch1 <- 5
   close(ch1)
}()

go func() {
   time.Sleep(time.Second * 7)
   ch2 <- 7
   close(ch2)
}()

for {
   select {
   case x, ok := <-ch1:
      if !ok {
         ch1 = nil
      } else {
         fmt.Println(x)
      }
   case x, ok := <-ch2:
      if !ok {
         ch2 = nil
      } else {
         fmt.Println(x)
      }
   }
   if ch1 == nil && ch2 == nil {
      break
   }
}
fmt.Println("program end")

Select

default 分支避免阻塞

其他非 default 分支因通信未就绪,而无法被选择的时候执行的,这就给 default 分支赋予了一种“避免阻塞”的特性。

func tryRecv(c <-chan int) (int, bool) {
   select {
   case i := <-c:
      return i, true

   default:
      return 0, false
   }
}

func trySend(c chan<- int, i int) bool {
   select {
   case c <- i:
      return true
   default:
      return false
   }
}

实现超时机制

select {
case <-c:
   // ... do some stuff
case <-time.After(30 *time.Second):
   return
}

特别注意 timer 使用后的释放,尤其在大量创建 timer 的时候。

实现心跳机制

结合 time 包的 Ticker,我们可以实现带有心跳机制的 select。这种机制让我们可以在监听 channel 的同时,执行一些周期性的任务:

heartbeat := time.NewTicker(30 * time.Second)
defer heartbeat.Stop()
for {
   select {
   case <-c:
      // ... do some stuff
   case <- heartbeat.C:
      //... do heartbeat stuff
   }
}

用完 ticker 之后,也不要忘记调用它的 Stop 方法,避免心跳事件在 ticker 的 channel(上面示例中的 heartbeat.C)中持续产生。