Golang 语言使用 channel 并发编程

简介: Golang 语言使用 channel 并发编程

01

介绍

我们在之前的文章中介绍过 golang 使用 sync 和 context 并发编程。我在文末给出相关文章的链接,还没有阅读的读者推荐阅读一下。

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 接收和发送

为了便于理解,我们可以将 channel 理解为一个 FIFO 先进先出的队列,sender 向 channel 中发送数据,receiver 从 channel 中接收数据。发送和接收使用的操作符都是 <-,它们的区别是,发送时操作符在 channel 类型变量名的右边,接收时操作符在 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)
  }
}

阅读这段代码,我们声明一个 receiver 函数,它的返回值是一个接收 channel。main 函数中,通过调用 receiver 函数,声明一个 <-chan int 类型的 channel 变量 c1。并且使用 for ... range 遍历 c1。

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。

代码中的 runTask 函数,返回值是 chan int,被用于两个 goroutine 之间的通知信号。该信号用于通知 main goroutine,runTask 函数所在的 goroutine,先调用 firstTask 函数,并被阻塞 100 毫秒,main goroutine 先执行 secondTask 函数,当 runTask 函数返回值赋值给变量 c 后,main goroutine 打印输出变量 c 的值。

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 替代锁的内容留给大家自己去实现。

推荐阅读:

参考资料:

https://golang.org/doc/codewalk/sharemem/

https://tour.golang.org/concurrency/2

https://gobyexample.com/channels


目录
相关文章
|
2月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
98 4
Golang语言之管道channel快速入门篇
|
2月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
62 4
Golang语言文件操作快速入门篇
|
2月前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
92 3
Golang语言之gRPC程序设计示例
|
2月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
80 4
|
2月前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
46 4
Golang语言goroutine协程篇
|
2月前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
46 3
Golang语言之Prometheus的日志模块使用案例
|
21天前
|
前端开发 中间件 Go
实践Golang语言N层应用架构
【10月更文挑战第2天】本文介绍了如何在Go语言中使用Gin框架实现N层体系结构,借鉴了J2EE平台的多层分布式应用程序模型。文章首先概述了N层体系结构的基本概念,接着详细列出了Go语言中对应的构件名称,包括前端框架(如Vue.js、React)、Gin的处理函数和中间件、依赖注入和配置管理、会话管理和ORM库(如gorm或ent)。最后,提供了具体的代码示例,展示了如何实现HTTP请求处理、会话管理和数据库操作。
25 0
|
2月前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
45 3
|
2月前
|
Go
Golang语言之函数(func)进阶篇
这篇文章是关于Golang语言中函数高级用法的教程,涵盖了初始化函数、匿名函数、闭包函数、高阶函数、defer关键字以及系统函数的使用和案例。
48 3
Golang语言之函数(func)进阶篇
|
2月前
|
Go
Golang语言之函数(func)基础篇
这篇文章深入讲解了Golang语言中函数的定义和使用,包括函数的引入原因、使用细节、定义语法,并通过多个案例展示了如何定义不返回任何参数、返回一个或多个参数、返回值命名、可变参数的函数,同时探讨了函数默认值传递、指针传递、函数作为变量和参数、自定义数据类型以及返回值为切片类型的函数。
50 2
Golang语言之函数(func)基础篇