go语言中channel类型

目录

一、什么是channel

二、为什么要有channel

三、channel操作使用

初始化

操作

单向channel

双向channel,可读可写

四、close下什么场景会出现panic

五、总结


一、什么是channel

Channels are a typed conduit through which you can send and receive values with the channel operator, <-.

channel是go语言的核心类型之一,翻译为中文是"通道,管道",为了实现协程间的同步与通信。遵循FIFO(先进先出)的队列,保证线程安全。

二、为什么要有channel

"不要用共享内存来通信,而是使用通信来共享内存" -- go语言并发哲学

任何一种程序语言要实现并发能力,就要做好多线程之间的协调工作,让彼此知道对方状态,获取对方的信息,完成预定任务。大多数的编程语言的并发编程模型是基于线程和内存同步访问控制,go语言的并发编程的模型则用 goroutine 和 channel 来实现。channel是goroutine之间架了一条管道,在管道里传输数据,实现goroutine间的通信来协调工作(当然goroutine实现通信不止channel一种,还有 go语言中协程实现通信的三种方式 context\sync.cond\channel)。

在go语言中,CSP(Communicating Sequential Processes)模型是go语言并发编程哲学的实现(三种线程模型与CSP实现),goroutine与channel是CSP上层实现的两大基石。

channel的底层实现保证了协程操作安全:在任何同一时间内,channel中的一个数据只允许一个协程访问,不存在数据竞争。

三、channel操作使用

初始化

channel是引用类型,有带缓冲channel和无缓冲channel,未初始化的channel值是nil。通过内建函数make (仅对map\slice\channel初始化)分配内存并初始化。

操作

channel只有三种操作方式Send、Receive、close。

通过操作符 <- 实现发送或读取数据(中文社区更愿意把Send和接收,从通信操作符号看chan <- 是发送数据到chan,<- chan是接收chan中数据,这是从通信角度理解。从读写角度理解,我更愿意翻译为chan <- 为写入数据到chan,<-chan为从chan读取数据出来)。数据为go中任意类型:

close为关闭chan:close(chan)

虽然go语言采自动垃圾回收机制来管理内存,但go的垃圾回收器不会主动回收运行中的channel, 主动关闭channel为了防止内存泄露。

单向channel

单向channel分为write-only,read-only channel,主要作用有限制通信方向、减少竞态条件、提高代码可读性。

限制通信方向:使用只写通道可以在某些情况下限制通信的方向,确保特定的协程只能发送数据到通道,而不会在不适当的地方进行接收操作。这有助于清晰地定义协程之间的职责和交互。

减少竞态条件:当只有一个协程负责向通道发送数据,而其他协程只负责接收时,可以减少竞态条件的出现。这有助于避免数据竞争和复杂的同步问题。

提高代码可读性:通过使用只写通道,你可以在代码中清楚地表达协程的作用。这有助于其他开发人员更容易地理解代码并阅读文档。

Go 复制代码
unc worker(id int, jobs <-chan int, results chan<- int) {
   for job := range jobs {
      fmt.Printf("Worker %d started job %d\n", id, job)
      time.Sleep(time.Millisecond)
      fmt.Printf("Worker %d finished job %d\n", id, job)
      results <- job * 2
   }
}

func main() {
   numJobs := 5
   jobs := make(chan int, numJobs)
   results := make(chan int, numJobs)
   // 启动3个工作协程
   for i := 1; i <= 3; i++ {
      go worker(i, jobs, results)
   }
   // 向通道发送任务
   for j := 1; j <= numJobs; j++ {
      jobs <- j
   }
   close(jobs)
   // 收集结果
   for r := 1; r <= numJobs; r++ {
      result := <-results
      fmt.Println("Result:", result)
   }
}

在这个示例中,我们使用只写通道 chan<- 来传递任务给工作协程,工作协程的<- chan只负责从通道中接收任务。这种模式将任务分发和执行解耦,增加了代码的可读性和可维护性。

总之,单向channel在go语言中用于限制通道的使用方向,有助于提高代码的可读性、降低竞态条件和减少复杂性。

双向channel,可读可写

基本通道使用:创建一个通道,发送数据到通道,然后从通道接收数据

Go 复制代码
func base() {
   ch := make(chan int) // 创建一个通道
   go func() {
      ch <- 42 // 发送数据到通道
   }()
   value := <-ch                   // 从通道接收数据
   fmt.Println("Received:", value) // Received: 42
}

使用缓冲通道:创建带有缓冲区的通道,可以存储多个数据,然后使用循环向通道发送和接收数据。

Go 复制代码
func cacheChan() {
   ch := make(chan int, 2) // 创建一个容量为2的缓冲通道
   ch <- 1
   ch <- 2
   value1 := <-ch
   value2 := <-ch
   fmt.Println("Received:", value1, value2) // Received: 1 2
}

协程池: 使用通道来实现一个简单的协程池,从通道中获取任务并分发给协程进行处理。

Go 复制代码
func worker(id int, jobs <-chan int, results chan<- int) {
   for job := range jobs {
      fmt.Printf("Worker %d started job %d\n", id, job)
      time.Sleep(time.Millisecond)
      fmt.Printf("Worker %d finished job %d\n", id, job)
      results <- job * 2
   }
}

func goroutinePool() {
   numJobs := 5
   numWorkers := 3
   jobs := make(chan int, numJobs)
   results := make(chan int, numJobs)
   
   for i := 1; i <= numWorkers; i++ {
      go worker(i, jobs, results)
   }
   for j := 1; j <= numJobs; j++ {
      jobs <- j
   }
   close(jobs)

   var wg sync.WaitGroup
   wg.Add(numJobs)
   go func() {
      wg.Wait()
      close(results)
   }()

   for r := range results {
      fmt.Println("Result:", r)
      wg.Done()
   }
}

取消协程: 使用通道来实现协程的取消,通过发送信号告知协程停止工作。

Go 复制代码
func worker(cancel <-chan struct{}) {
   for {
      select {
      case <-cancel:
         fmt.Println("Worker canceled")
         return
      default:
         fmt.Println("Working...")
         time.Sleep(time.Second)
      }
   }
}

func cancelRoutine() {
   cancel := make(chan struct{})
   go worker(cancel)
   time.Sleep(3 * time.Second)
   fmt.Println("Canceling worker...")
   close(cancel)
   time.Sleep(1 * time.Second)
}

四、close下什么场景会出现panic

在使用channel时,为了获得良好的协程同步与通信结果,在一些场景下会导致程序panic,

如下为读写与channel状态对协程的影响表:

调用close关闭channel时,未初始化时关闭、重复关闭、关闭后发送、发送时关闭,在这四种情况下会出现panic:

未初始化就关闭

Go 复制代码
func main() {
   var ch chan int
   close(ch) // panic: close of nil channel
}

重复关闭

Go 复制代码
func main() {
   ch := make(chan int)
   close(ch)
   close(ch) // panic: close of closed channel
}

关闭后发送

Go 复制代码
func main() {
   wg := sync.WaitGroup{}
   wg.Add(1)
   ch := make(chan int)
   close(ch)

   go func() {
      defer wg.Done()
      ch <- 1 // panic: send on closed channel
   }()
   <-ch
   wg.Wait()
}

发送后关闭

Go 复制代码
func main() {
   ch := make(chan int)
   var wg sync.WaitGroup
   wg.Add(1)

   go func() {
      defer wg.Done()
      defer fmt.Println("close ch") // close ch
      defer close(ch)
      go func() {
         ch <- 1 // panic: send on closed channel
      }()
   }()
   fmt.Println(<-ch)
   wg.Wait()
}

不正确地关闭channel会导致程序panic,如何优雅关闭channel呢?

参看:《How to Gracefully Close Channels》

五、总结

本内容主要讲述了channel的基础知识,包括定义、使用背景、类型、操作方式、使用场景、不正确关闭channel会panic的场景。没有对channel的底层实现原理进行解读,参看channel的底层实现原理了解。

相关推荐
秃头佛爷41 分钟前
Python学习大纲总结及注意事项
开发语言·python·学习
待磨的钝刨42 分钟前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
XiaoLeisj3 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
励志成为嵌入式工程师4 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉4 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
记录成长java6 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山6 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
睡觉谁叫~~~6 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust