golang channel和select

Channel

  • 通道可以传输 int, string, 结构体,甚至是接口类型变量,函数和另一个channel
type sub struct {
    closing chan chan error
}

func (s *sub) Close() error {
    errc := make(chan error)
    s.closing <- errc
    return <-errc
}
  • 通道默认值是nil,需要通过make来初始化
  • 通道传递是拷贝值
    • 对于大数据类型,可以传递指针以避免大量拷贝
      • 注意此时的并发安全,即多个goroutine通过指针对原始值的并发操作
      • 此时需要额外的同步操作(例如锁)来避免竞争
  • 缓冲通道和无缓冲通道
    • ch := make(chan bool)
      • 无缓存的channel是同步的,阻塞式的
        • 必须等待两边都准备好才开始传递数据,否则堵塞
        • 一般用来同步各个的goroutine
      • 使用不当容易引发死锁
    • ch := make(chan int, 10)
      • 有缓存的channel是异步的,只有当缓冲区写满或读空时才堵塞
      • 等待的、所有接收操作所在的 goroutine,都会按照先后顺序被放入通道内部的接收等待队列
        • 可以发送或读取后,通道会优先通知最早因此而等待的goroutine
  • 通道的关闭
    • channel 使用完后不关闭也没有关系

      • 因为channel 没有被任何协程用到后会被自动回收
      • 显式关闭 channel 一般是用来通知其他协程某个任务已经完成了
    • 应该是生产者关闭通道,而不是消费者

      • 否则生产者可能在channel关闭后写入,导致panic
      • 消费者在超时后应该通过channel向生产者发送完成消息,让生产者关闭channel并返回
    • 关闭一个已经关闭的channel,或者往一个已经关闭的channel写入,都会panic

    • 已经被关闭的通道不能往里面写入,但可以接受数据

      • 读取一个已经关闭且没有数据的通道会立刻返回一个零值
      • 读取时通过判断第二个返回值也可以判读收到的值是否有效
  • 单向通道
    • 多用于函数的参数, 提高安全性
      • 只能写入:var send chan<- int
      • 只能读取:var recv <-chan int
    • 这种约束一般会出现在接口类型声明和函数声明汇总
  • 使用for-range循环读取channel
    • 从指定通道中读取数据直到通道关闭(close)
      • 如果生产者忘记关闭通道,则消费者会一直堵塞在for-range循环中
    • 如果ch值为nil,则会那么这条for语句就会被永远地阻塞在有for关键字的那一行

原理

  • channel本质是一个结构体
    • 所谓的发送数据到channel,或者从channel读取数据,说白了就是对这个结构体的操作

在这里插入图片描述

  • channel可以用来无锁编程,但是channel本身底层还是通过加锁实现的
    • 单次传递更多数据可以改善因为频繁加锁造成的性能问题
    • 例如把make(chan int, bufsize)改为make(chan [blocksize]int, bufsize)
      • 其中blocksize是常量

在这里插入图片描述

例子

  • channel作为函数返回值
    • 一般作为生产者,另起一个goroutine并发生产,返回channel用于消费
    • 应该通过闭包提供给消费者关闭该协程的函数
      • 使用defer把goroutine的生命周期封装在生产函数中
        • 目的在于避免写入nil或者多次关闭channel
    • 消费者只需要处理阻塞和零值,生产者负责在生产完毕后关闭channel
      在这里插入图片描述
func producer(generator func() int) (<-chan int, func()) {
	ch := make(chan int)
	done := make(chan struct{})
	go func() {
		defer close(ch) // 重要!
		for {
			select {
			case <-done:
				return
			default:
				ch <- generator()
			}
		}
	}()

	return ch, func() {close(done)} // 通过闭包提供关闭函数
}
  • 用channel进行同步
type Stream struct {
    // some fields
    cc chan struct{}
}

func (s *Stream) Wait() error {
    <-s.cc
    // some code
}
func (s *Stream) Close() {
    // some code
    close(s.cc)
}
func (s *Stream) IsClosed() bool {
    select {
    case <-s.cc:
        return true
    default:
        return false
    }
}
  • 实现信号量
var wg sync.WaitGroup

sem := make(chan struct{}, 5) // 最多并发5个
for i := 0; i < 100; i++ {
	wag.Add(1)
	go func(id int) {
		defer wg.Done()
		sem <- struct{}{} // 获取信号量
		defer func(){
			<-sem // 释放信号量
		}()
		
		// 业务逻辑
		...
	}(i)
}

wg.Wait()
  • 用channel限制速度
limiter := time.Tick(time.Millisecond * 200)
// 每 200ms 执行一次请求
for req := range requests {
    <-limiter
    ...
}
  • 流水线函数写法
func pipeline(in <-chan *Data, out chan<- *Data) {
	for data := range in {
		out <- process(data)
	}
}

// 使用
go pipeline(in, tmp)
go pipeline(tmp, out)	
  • 由于channel本身是一个并发安全的队列,因此可以用作Pool
type pool chan []int // []int 可以用任何对象代替

func newPool(cap int) pool {
    return make(chan []int, cap)
}

func (p pool) get() []int {
    var v []int
    
    select {
    case v = <-p:
    default:
         v = make([]int, 10)
    }
    
    return v
}

func (p pool) put(in []int) {
    select {
    case p <- in: //成功放回
    default:
    }
}

Select

  • select语句是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达式
  • select 默认阻塞,只有监听的channel中有发送或者接受数据时才运行
    • 设置default则不阻塞,通道内没有待接受的数据则执行default
      • 如果不加default,则会有死锁风险
  • 多个channel准备好时,会随机选一个执行
    在这里插入图片描述
  • select语句包含的候选分支中的case表达式都会在select语句执行开始时先被求值
    • 所以time.After可以使用在select中
    • 求值的顺序是依从代码编写的顺序从上到下
    • 仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支
  • 如果我们想连续或定时地操作其中的通道的话,就需要通过在for语句中嵌入select语句的方式实现
    • 注意简单地在select语句的分支中使用break语句,只能结束当前的select语句的执行,而并不会对外层的for语句产生作用
  • select{}永远阻塞

例子

  • 利用channel+select来广播退出信息
    • 每个子goroutine利用select监听done通道
    • 当主程序想要关闭子goroutine时,可以关闭done通道
    • 此时select会立刻监听到nil消息,子goroutine可以以此退出
func Generate(done chan bool) chan int {
	ch := make(chan int)
	go func() {
		defer close(ch)
		for {
			select{
			case ch <- rand.Int():
				...
			case <- done: // 接受到通知并退出
				return
			}
		}	
	}()
	
	return ch
}

done := make(chan bool)
ch := Generate(done)
fmt.Println(<-ch) // 消费
close(done) //通过关闭通道来发送通知
  • 如果在select语句中发现某个分支的通道已关闭,那么这个分支会一直被执行
    • 为了防止再次进入这个分支,可以把这个channel重新赋值为nil,这样这个case就一直被阻塞了
for {
	select {
	case x, open := <-inCh1:
		if !open {
			inCh1 = nil
			break
		}
		out<-x
	case x, open := <-inCh2:
		if !open {
			inCh2 = nil
			break
		}
		out<-x
	}

	// 当ch1和ch2都关闭是才退出
	if inCh1 == nil && inCh2 == nil {
		break
	}
}

  • 单个case的化简写法
// bad
select {
	case <-ch:
}
// good
<-ch

// bad
for { 
	select {
	case x := <-ch:
		_ = x
	}
}

//good
for x := range ch {
   ...
}
  • 非阻塞写入/读取channel
select {
case ch <- struct{}{}:
default:
}

select {
case val := <-ch:
	...
default:
}
  • 6
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值