开发者

Golang中channel的用法举例详解

目录
  • 前言
  • 1.channel的类别
  • 2.channel的状态
  • 3.channel的操作
  • 4.组合操作
    • 4.1 for range closed_chan
    • 4.2 nil channel在select中的行为
  • 5.channel常见用法
    • 5.1 使用for range读取channel
    • 5.2 使用_, ok判断channel状态
    • 5.3 使用select进行多路channel处理
    • 5.4 使用channel控制读写权限
    • 5.5 使用channel进行并发控制
    • 5.6 超时控制
    • 5.7 非阻塞读写channel
    • 5.8 级联close channel
    • 5.9 信号事件载体
    • 5.10高效数据传输
  • 总结

    前言

    在golang并发编程实践中,channel的正确运用直接影响程序的健壮性和执行效率。本文将深入探讨几种提升channel使用效能的编程客栈典型场景与实现策略。

    1.channel的类别

    主要有2种:

    有缓存 Channel(buffered channel),使用 make(chan type, capacity int) 创建

    无缓存 Channel(unbuffered channel),使用 make(chan type) 创建

    其中 ,type为 Channel 传递数据的类型,capacity 为缓存大小。

    unbuffered channel:阻塞、同步模式

    • sender端向channel中send一个数据,然后阻塞,直到receiver端将此数据receive
    • receiver端一直阻塞,直到sender端向channel发送了一个数据

    buffered channel:非阻塞、异步模式

    • sender端可以向channel中send多个数据(只要channel容量未满),容量满之前不会阻塞
    • receiver端按照队列的方式(FIFO,先进先出)从buffered channel中按序receive其中数据

    2.channel的状态

    主要有3种状态:

    • actived,正常状态,可以正常的读receive,写send
    • nil,未初始化状态,即只进行了声明但还尚未分配内存,或者是channel被主动赋值为了nil
    • closed,关闭状态。 注意:channel被close后,它的状态并不是nil。因为nil channel是不能读取的,会panic。但是close后的channel,如果管道里还有数据,是可以通过range正常读取出来的。

    3.channel的操作

    常用的操作有这4种:

    • 读,<- ch
    • 写,ch <-
    • 关闭, close(ch)
    • 遍历,for v := range ch {}

    4.组合操作

    前面所说的3种状态,和4种操作,组合起来后的结果如下:

    操作\状态activedclosenil
    <-ch (读)成功或者阻塞零值死锁
    ch<- (写)成功或者阻塞panic死锁
    close成功panic(重复关闭)panic
    for range成功成功(break)死锁

    有2个特殊点需要说明:

    4.1 for range closed_chan

    场景1:channel已关闭且无剩余数据循环立即退出:如果channel在关闭时已经没有数据,for range循环不会执行任何迭代,直接终止。

    ch := make(chan int)
    close(ch)
    for v := range ch { // 循环不执行,直接退出
        // 代码不会执行
    }
    

    场景2:channel已关闭但有剩余数据读取所有数据后退出:如果channel关闭时仍有数据未被读取,for range会读取所有剩余数据,然后正常退出循环。

    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    close(ch)
    for v := range ch { 
        fmt.Println(v) // 输出1、2
    }
    

    场景3:未关闭的Channel会导致阻塞如果channel未关闭且无数据,for range会一直阻塞等待数据,可能导致goroutine泄漏。

    4.2 nil channel在select中的行为

    如上面的表格所展示:对nil channel进行读写,都会导致死锁(永久阻塞)。

    无论是发送(ch <- v)还是接收(<- ch),操作nil channel的代码会永久阻塞。

    var ch chan int // ch是nil
    ch <- 1         // 永久阻塞(发送到nil channel)
    <-ch            // 永久阻塞(从nil channel接收)
    

    但是,如果是在select中,则select会忽略nil channel的case:

    • 当select的某个case操作的是nil channel时,该case会被视为未就绪,直接跳过。
    • 如果其他case中有就绪的channel操作,select会正常执行这些case。
    • 如果所有case都未就绪(包括nil channel的case),且没有default分支,则select会阻塞等待,但不会触发死锁(除非整个goroutine再无其他代码可执行)。

    场景1:nil channel与其他有效case共编程客栈

    var ch chan int // ch是nil
    timeout := time.After(1 * time.Second)
    
    select {
    case <-ch: // 该case被跳过(nil channel)
        fmt.Println("Receivandroided from ch")
    case <-timeout:
        fmt.Println("Timeout") // 1秒后执行
    }
    

    结果:select会忽略<-ch(nil channel),等待timeout就绪后执行。

    不会死锁:因为存在其他有效case(timeout)。

    场景2:所有case均为nil channel

    var ch1, ch2 chan int // 均为nil
    select {
    case <-ch1: // 被跳过
    case <-ch2: // 被跳过
    }
    

    结果:select会永久阻塞,但如果整个goroutine没有其他代码可执行,会触发死锁(fatal error: a编程ll goroutines are asleep - deadlock!)。

    关键点:死锁是否发生取决于整个goroutine的状态,而不仅仅是select本身。

    select的设计机制:

    • select会动态检查所有case的就绪状态,跳过未就绪的case(包括nil channel)。
    • 只要存在其他就绪的case(如定时器、非nil channel等),select就能正常执行。

    5.channel常见用法

    5.1 使用for range读取channel

    场景:需要持续不断从channel读取数据

    说明:采用range关键字进行通道遍历,当发送端关闭通道时,循环自动终止。这种方式避免了手动检测通道状态的繁琐操作,同时保证不会读取到无效零值。

    示例:

    for v := range ch{
        DOSomething(v)
    }
    

    5.2 使用_, ok判断channel状态

    场景:双返回值验证机制,用于读取channel但不确定channel是否已经关闭

    说明:当不确定通道是否关闭时,采用特殊语法进行安全校验。返回值状态指示符ok为true表示成功接收有效数据,false则标志通道已关闭。

    示例:

    if v, ok := <-ch; ok {
        doSomething(v)
    }
    

    5.3 使用select进行多路channel处理

    场景:选择性路由机制。select对多个channel同时处理时,会先处理最先发生的channel

    说明:当需要同时监听多个数据源时,select语句可实现智能路由。注意nil channel的特殊处理,读取会永久阻塞,写入将导致运行时异常,但是注意4.2节中提到的select对于nil channel的特殊情况。

    示例:

    select {
    case taskCh <- newTask:
        processTask()
    case <-shutdownSignal:
        terminate()
    }
    

    5.4 使用channel控制读写权限

    场景:协程对某个channel只读或者只写时

    说明:通过声明只读/只写类型channel,增强代码可维护性。这种类型约束可防止意外的反向操作,降低运行时panic风险。

    示例:

    // 只写操作
    func writeOnly(n int)  <-chan int {
        ch := make(chan int)
        go func() {
          defer close(ch) //必须关闭channel以通知外面的其它工作协程退出.
                          //不然,在外面无法close一个单向的channel,导致死锁
          for i:=0; i<n; i++ {
              ch <- i
          }
        }()
        return out
    }
    
    // 只读操作
    func readOnly(in <-chan int) {
        for v := range in {
            doSomething(v)
        }
    }
    

    5.5 使用channel进行并发控制

    场景:同步,异步和并发调用

    说明:有缓存chan(buffered channel)是异步的,可提供给多个协程同时处理,提高系统的并发性能。而无缓存chan(unbuffered channel)是同步的。

    // 有缓存channel
    ch1 := make(chan int, 1)
    // 无缓存channel
    ch2 := make(chan int)
    ch3 := make(chan int, 0)
    

    示例: 并发处理模型

    func doWorker(inCh <-chan int, outCh chan<- int, wg *sync.WaitGroup) {
    	defer wg.Done()
    	for v := range inCh {
    		outCh <- v * 10
    	}
    }
    
    func concurrentProcess() {
    	inCh := writeOnly(100)
    	outCh := make(chan int, 10)
    	var wg sync.WaitGroup
    
    	// 同时运行5个协程,从inCh中并发读取数据,并发写入outCh
    	for i := 0; i < 5; i++ {
    		wg.Add(1)
    		go doWorker(inCh, outCh, &wg)
    	}
    
    	//等待所有worker完成并关闭outCh
    	go func() {
    		wg.Wait()
    		close(outCh)
    	}()
    
    	for v := range outCh {
    		fmt.Println(v)
    	}
    }
    

    5.6 超时控制

    场景:在一些需要进行超时控制的情况下

    说明:通过结合定时器(select & time.After)实现操作时效控制,避免长期阻塞。特别注意定时channel的资源释放问题。

    示例:

    func doWorker() <-chan int {
    	ch := make(chjavascriptan int)
    	go func() {
    		// do something for ch
    	}()
    	return ch
    }
    	
    func doWithTimeout(timeout time.Duration) (int, error) {
    	select {
    	case v := <-doWorker2():
    		return v, nil
    	case <-time.After(timeout):
    		return 0, fmt.Errorf("timeout")
    	}
    }
    
    func main() {
    	v, err := doWithTimeout(1 * time.Second)
    	fmt.Printf("v:%d, err:%v\n", v, err)
    }
    
    

    输出: v:0, err:timeout

    5.7 非阻塞读写channel

    场景:对channel进行非阻塞式的读或者写

    说明:通过default分支实现即时返回,适用于不可阻塞的实时系统。需要与带缓冲通道配合使用。

    示例:

    非阻塞的立即返回方案

    func unblockWrite(ch chan int, v int) error {
    	select {
    	case ch <- v:
    		return nil
    	default:
    		return fmt.Errorf("channel write blocked")
    	}
    }
    
    func unblockRead(ch chan int) (int, error) {
    	select {
    	case v := <-ch:
    		return v, nil
    	default:
    		return 0, fmt.Errorf("channel read blocked")
    	}
    }
    

    5.8 级联close channel

    场景:进行优雅关闭,广播通知所有协程退出

    说明:关闭channel会产生广播效应,所有接收此channel的协程都会收到零值。结合sync.WaitGroup可实现安全终止。

    示例:

    type Manager struct {
    	stopCh chan struct{}
    	workCh chan struct{}
    	wg     sync.WaitGroup
    }
    
    func (m *Manager) Shutdown() {
    	close(m.stopCh)
    	//等待所有其它协程退出
    	m.wg.Wait()
    }
    
    func (m *Manager) workLoop() {
    	for {
    		select {
    		case v := <-m.workCh:
    			go doWorker(v)
    		case <-m.stopCh:  //close后会读取到零值
    			return
    		}
    	}
    }
    

    5.9 信号事件载体

    场景:定义的channel,仅用来传递事件/信号,无需传递数据

    说明:当仅需事件通知而不传递数据时,采用空结构体通道可最小化内存消耗。

    示例:在5.8节中,stopCh就是用于事件传递的channel。它并不需要传递数据,只需要向其它的所有协程发出终止信号。

    5.10高效数据传输

    场景:用于性能优化,传递指针,而非拷贝数据

    说明:对于大型数据结构,传递指针可显著降低通道操作的复制开销。需注意并发访问时的数据竞争问题。

    示例:

    type Payload struct {
        // 大数据结构
    }
    
    payloadChan := make(chan *Payload, 10)
    

    以上就是关于channel的一些实践技巧,合理运用能有效提升并发程序的执行效率和代码可维护性。开发者应根据具体场景选择适当的模式,并注意资源管理与并发安全问题。

    总结

    到此这篇关于Golang中channel用法举例详解的文章就介绍到这了,更多相关Golang中channel用法内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜