Golang 语言使用 channel 并发编程

01

介绍

golang 作者 Rob Pike 说过一句话,不要通过共享内存来通信,而应该通过通信来共享内存。怎么通过通信来共享内存呢?使用 channel 可以实现 Rob Pike 说的通过通信来共享内存,我们可以使用 channel 在多个 goroutine 中传递数据。今天我们来介绍一下 golang 使用 channel 并发编程,在介绍并发编程前,先介绍一下 channel。

channel 是并发安全的类型,同一时刻,运行时只会执行一个对同一 channel 操作(发送或接收)中的某一个操作(发送或接收),即操作(发送或接收)之间是互斥的。并且对同一 channel 中的同一个元素执行的发送和接收操作之间也是互斥的。

02

无缓冲和有缓冲 channel

在 golang 中,声明 channel 需要使用 make 函数,第一个参数是 chan int,它是 channel 中数据的数据类型,第二个参数是可选参数 cap,它是一个 int 类型的参数,且值必须大于 0。如果没有给定第二个参数,该 channel 为无缓冲 channel。反之为有缓冲 channel。

// 声明一个 int 类型的无缓冲 channel
c1 := make(chan int)
// 声明一个 int 类型的有缓冲 channel,容量 cap 为 5
c2 := make(chan int, 5)

03

channel 接收和发送

-
c := make(chan int, 2)
// send
c - 1
c - 2
// receive
- c
// 接收并赋值给变量
x - c

04

channel 关闭

使用 close 函数可以关闭 channel,channel 关闭后,发送方继续向 channel 中发送数据会引发 panic,但是接收方可以感知到 channel 关闭,并且可以安全退出。

注意:关闭一个已经关闭的 channel,也会引发 panic。

我们可以通过接收第二个返回值,判断当前 channel 是否关闭。

c := make(chan int, 5)
close(c)
val, ok := - c
fmt.Println(val, ok)

第二个返回值为 true,表示 channel 未关闭,反之表示已关闭。

05

单向 channel

我们以上讲述的都是双向 channel,即可以向 channel 发送数据,也可以从 channel 接收数据。此外,还有单向 channel,即只可以对 channel 进行发送或接收数据的操作。

// 单向 channel,只能发送不能接收(发送 channel)
c1 := make(chan- int, 5)
// 单向 channel,只能接收不能发送(接收 channel)
c2 := make(-chan int, 5)

可能有读者要问了,channel 不就是为了传递数据的吗?单向 channel 只能发送或只能接收,无法传递数据,有什么意义吗?

是的,单向 channel 主要用于约束的作用。

// 返回一个 int 类型的单向 channel(接收 channel)
func receiver () -chan int {
  // 声明一个 int 类型的有缓冲 channel,容量是 5
  c := make(chan int, 5)
  // 向 channel 中发送数据
  for i := 0; i  5; i++ {
    c - i
  }
  // 关闭 channel
  close(c)
  return c
}

func main () {
  c1 := receiver()
  for i := range c1 {
    fmt.Println(i)
  }
}
-chan intfor ... range

06

使用 channel 并发编程

前面的内容,我们主要介绍了使用 channel 在多个 goroutine 之间进行通信,本小节我们介绍使用 channel 在多个 goroutine 之间进行同步。channel 是 golang 提供的基于 CSP (Communicating Sequential Process)的并发原语,我们可以使用 channel 并发编程。

// 任务 1
func firstTask () int {
 time.Sleep(time.Millisecond * 100)
 fmt.Println(first task)
 return 1
}
// 任务 2
func secondTask() {
 fmt.Println(second task start)
 time.Sleep(time.Millisecond * 200)
 fmt.Println(second task end)
}
// 执行任务 1
func runTask() chan int {
 c := make(chan int)
 go func() {
  val := firstTask()
  c - val
 }()
 return c
}

// 并发执行任务 1 和任务 2
func main () {
  // 执行任务 1
 c := runTask()
  // 执行任务 2
 secondTask()
 fmt.Println(- c)
}

阅读上面这段代码,使用 channel 并发编程。为了避免和 sync 并发编程混淆,特意使用 time.Sleep() 替代 sync.WaitGroup。

chan int

07

select 多路选择和 time.After 超时

select 语句的用法类似于 switch 语句,但是 select 语句只能用于操作 channel。

func firstCh () -chan int {
 c := make(chan int, 1)
 go func() {
  time.Sleep(time.Millisecond * 200)
  c - 1
 }()
 return c
}

func secondCh () -chan int {
 c := make(chan int, 1)
 go func() {
  time.Sleep(time.Millisecond * 300)
  c - 2
 }()
 return c
}

func main() {
 select {
 case val, ok := - firstCh():
  if !ok {
   fmt.Println(channel is closed)
   break
  }
  fmt.Println(first channel success, val)
 case val, ok := - secondCh():
  if !ok {
   fmt.Println(channel is closed)
   break
  }
  fmt.Println(second channel success, val)
 // case -time.After(time.Millisecond * 100):
 //  fmt.Println(time out)
 // default:
 //  fmt.Println(default)
 }
}

阅读上面这段代码,我们使用 golang 并发原语 channel,结合 select 实现多个 goroutine 之间同步。

08

总结

本文我们介绍了 channel 在 golang 中的相关操作,和使用 channel 并发编程,即通过通信来共享内存的方式。其中使用 channel 并发编程内容中,是通过将 channel 作为 goroutine 之间的通知信号,此外,还可以通过 channel 替代锁。限于篇幅,关于 channel 替代锁的内容留给大家自己去实现。