golang channel和select

Channel

  • 通道可以传输 int, string, 结构体,甚至是接口类型变量和函数

  • 通道传递是拷贝值

    • 对于大数据类型,可以传递指针以避免大量拷贝
      • 注意此时的并发安全,即多个goroutine通过指针对原始值的并发操作
      • 此时需要额外的同步操作(例如锁)来避免竞争
  • 缓冲通道和无缓冲通道

    • ch := make(chan bool)
      • 无缓存的channel是同步的,阻塞式的
        • 必须等待两边都准备好才开始传递数据,否则堵塞
        • 一般用来同步各个的goroutine
      • 使用不当容易引发死锁
    • ch := make(chan int, 10)
      • 有缓存的channel是异步的,只有当缓冲区写满时才堵塞
  • 通道的关闭

    • 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的数据结
      构甚至在访问的时候都不用加锁
      • 因为Go语言支持多CPU核心并发执行多个goroutine,会造成资
        源竞争,所以在必要的位置还是需要加锁的
  • channel可以用来无锁编程,但是channel本身底层还是通过加锁实现的

    • 单次传递更多数据可以改善因为频繁加锁造成的性能问题
    • 例如把make(chan int, bufsize)改为make(chan [blocksize]int, bufsize)
      • 其中blocksize是常量

golang channel和select_第1张图片

例子

  • channel作为函数返回值
    • 一般作为生产者,另起一个goroutine并发生产,返回channel用于消费
    • 应该通过闭包提供给消费者关闭该协程的函数
    • 这也是一种惰性生成器
    • 使用defer把goroutine的生命周期封装在生产函数中
      • 目的在于避免写入nil或者多次关闭channel
    • 消费者只需要处理阻塞和零值,生产者负责在生产完毕后关闭channel
      golang channel和select_第2张图片
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表达式各求值一次,如果我们想连续或定时地操作其中的通道的话,就需要通过在for语句中嵌入select语句的方式实现
    • 注意简单地在select语句的分支中使用break语句,只能结束当前的select语句的执行,而并不会对外层的for语句产生作用
    • 这种错误的用法可能会让这个for语句无休止地运行下去

例子

  • 利用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 _, ok := <-ch1:
		if !ok {
			ch1 = nil
		}
	}
			
    if ch == nil { 
       break
    }
}

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

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

//good
for x := range ch {
   ...
}

你可能感兴趣的:(Golang)