GO并发编程基础- 如何使用channel

一、channel 入门

1. 什么是 channel

通道,顾名思义,使用来传递数据信号的,和队列类似,有发送端和接收端,也是先进先出的数据结构,通常是在不同的 goroutine 做通信处理,结合 select ,是 GO 并发编程中的武功秘籍

2. channel 的分类

channel 有两种类型,有缓存的channel 和无缓存的channel。
无缓冲 channel 的运行时层实现不带有缓冲区,所以 Goroutine 对无缓冲 channel 的接收和发送操作是同步的。也就是说,对同一个无缓冲 channel,只有对它进行接收操作的 Goroutine 和对它进行发送操作的 Goroutine 都存在的情况下,通信才能得以进行,否则单方面的操作会让对应的 Goroutine 陷入挂起状态

3. channel 的使用

3.1 创建一个channel
  • 无缓存的 channel
var ch1 chan struct{} // nil, 不能往 nil 里面发送数据
ch2 := make(chan struct{}) // 非 nil
  • 有缓存的 channel
ch3 := make(chan struct{}, 2)
3.2 channel 发送和接收数据

通道的发送和接收数据都是使用了 <- 符号

  • 发送数据
ch2 := make(chan struct{}, 2) 
ch2 <- struct{}
ch2 <- struct{}
  • 接收数据
var temp int

temp <- ch2
<-ch2

for _,v := range ch2 {
    ...
}

对通道进行操作的时候,会有一些阻塞的场景。

  • 当通道满的时候,发送操作会被阻塞。
func main() {
	go func() {
		ch := make(chan struct{}, 2)
		ch <- struct{}{}
		ch <- struct{}{}
		ch <- struct{}{} // 阻塞,后面代码不会执行
		fmt.Println("over")
	}()
	time.Sleep(time.Second * 5)
	fmt.Println("all over")
	}
  • 向一个空的通道拿数据,也会被阻塞
func main() {
	ch1 := make(chan int)
	go func() {
		fmt.Println("等待入队数据")
		time.Sleep(time.Second * 3)
		ch1 <- 1 
		fmt.Println("入队结束!")
	}()
	// 等待入队数据,阻塞
	<-ch1
	fmt.Println("over")
}

对无缓冲 channel 类型的发送与接收操作,一定要放在两个不同的 Goroutine 中进行,否则会导致 deadlock
例如:

func main() {
	ch1 := make(chan int, 0)
	ch1 <- 1
	<-ch1
	fmt.Println("over")
}
3.3 关闭 channel
  • channel 关闭后,所有等待从这个 channel 接收数据的操作都将返回
func main() {
	ch := make(chan int, 5)
	for i := 0; i < 5; i++ {
		go func() {
			i := <-ch // 阻塞
			fmt.Println(i, "over!")
		}()
	}
	ch <- 1
	ch <- 2
	ch <- 3
	time.Sleep(time.Second * 2) // 保证前面的代码都走完了
	close(ch)
	time.Sleep(time.Second * 2) // 保证 close 后,剩余的 goroutine 可以监听并处理
	fmt.Println("all over!")
}
  • 当你关闭一个已经被关闭的channel 时,会 Panic
func main() {
	ch := make(chan int)
	close(ch)
	close(ch)
}

  • 接收数据时,可以判断 channel 是否已关闭
m, ok := <-ch // 当ch被关闭后,m将被赋值为ch元素类型的零值, ok值为false

for v := range ch {
// 当ch被关闭后,for range循环结束 ... ...
    
}

因为发送端没有像接受端那样的、可以安全判断 channel 是否被关闭了的方法, 所以一般关闭 channel 的操作都是在发送端

  • 往已经关闭的 channel 里发送数据,也会触发 Panic
func main() {
	ch := make(chan int)
	close(ch)
	ch <- 1
}
3.4 select 的使用

通过 select,我们可以同时在多个 channel 上进行发送 / 接收操作:


select {
case x := <-ch1:     // 从channel ch1接收数据
  ... ...

case y, ok := <-ch2: // 从channel ch2接收数据,并根据ok值判断ch2是否已经关闭
  ... ...

case ch3 <- z:       // 将z值发送到channel ch3中:
  ... ...

default:             // 当上面case中的channel通信均无法实施时,执行该默认分支
}

当 select 语句中没有 default 分支,而且所有 case 中的 channel 操作都阻塞了的时候,整个 select 语句都将被阻塞,直到某一个 case 上的 channel 变成可发送,或者某个 case 上的 channel 变成可接收,select 语句才可以继续进行下去

二、channel 实战

1. 无缓冲通道的常用做法

  • 信号传递- 1-1 通知
package main

import (
	"fmt"
	"time"
)

type sign struct{} // 信号

func work() {
	fmt.Println("working start")
	time.Sleep(time.Second * 2) // 假装工作
	fmt.Println("working end")
}

func process(f func()) <-chan sign {
	ch := make(chan sign)
	go func() { // 要单独起一个协程,通道的操作通常不能在同一个协程里
		fmt.Println("process start")
		f()
		ch <- sign{}
		fmt.Println("process end")
	}()
	return ch
}

func main() {
	c := process(work)
	<-c
}
  • 信号传递- 广播通知
func main() {
	var wg sync.WaitGroup
	ch := make(chan int)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(i int) {
			fmt.Println(i, "大哥我准备好了!")
			wg.Done()
			<-ch // 阻塞
			fmt.Println(i, "干活了!")
		}(i)
	}
	wg.Wait()
	time.Sleep(time.Second * 3)
	fmt.Println("兄弟们,开工了!")
	close(ch) // 广播通知大家干活了
	time.Sleep(time.Second * 3)
}
  • 锁机制
package main

import (
	"fmt"
	"sync"
)

// 利用无缓冲队列的阻塞原理,满的通道输出数据时会阻塞,通过通道的操作来控制执行
var nums int
var ch = make(chan int)

func count() {
	// 单独起一个协程负责加数
	go func() {
		for {
			ch <- nums // 满的时候会阻塞
			nums++
		}
	}()

}

func main() {
	var wg sync.WaitGroup
	count()
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func(i int) {
			<-ch
			wg.Done()
		}(i)
	}
	wg.Wait()
	fmt.Println(nums)
}

2. 缓冲通道的常用做法

  • 消息队列
package main

import (
	"fmt"
	"time"
)

var ch chan int

func initQueue(i int) {
	ch = make(chan int, i)
}

func enqueue(i int) {
	if isFull() {
		return
	}
	ch <- i
}

func dequeue() int {
	return <-ch
}

func isEmpty() bool {
	if len(ch) == 0 {
		return true
	}
	return false
}

func isFull() bool {
	if len(ch) == cap(ch) {
		return true
	}
	return false
}

func product(nums int) {
	for i := 0; i < nums; i++ {
		go func(i int) {
			enqueue(i)
		}(i)
	}
}

func consumer(f func()) {
	ticker := time.NewTicker(time.Second * 5) // 定时器, 5s 后超时返回
	for {
		select {
		case v := <-ch:
			fmt.Println(v)
			f()
		case <-ticker.C:
			fmt.Println("handle over time , is over!")
			return
		default:
		}
	}
}

func main() {
	nums := 10
	initQueue(nums)
	product(nums)
	consumer(func() {
		fmt.Println("go!")
	})
}

  • 计数信号量

var active = make(chan struct{}, 3)
var jobs = make(chan int, 10)

func main() {
	go func() {
		for i := 0; i < 8; i++ {
			jobs <- i + 1
		}
		close(jobs) // 让 for 循环退出
	}()
	
	
	var wg sync.WaitGroup
	for j := range jobs {
		wg.Add(1)
		go func(j int) {
			active <- struct{}{} // 有三个正在消费的话,会阻塞
			log.Printf("handle job: %d\n", j)
			time.Sleep(2 * time.Second)
			<-active // 消费完,让出坑位
			wg.Done()
		}(j)
	}
	wg.Wait()
}

3. 通道的相关函数

  • len

当通道是非缓冲通道时, len(ch) 总是返回0,而当通道为缓冲通道时, len(ch) 返回的则是还未读取的通道元素个数, 需要注意的是,channel 原语用于多个 Goroutine 间的通信,一旦多个 Goroutine 共同对 channel 进行收发操作,len(channel) 就会在多个 Goroutine 间形成“竞态”。单纯地依靠 len(channel) 来判断 channel 中元素状态,是不能保证在后续对 channel 的收发时 channel 状态是不变的。

例如,A Goroutine 一开始判断通道不为空,准备读取数据,此时 B Goroutine 已经把数据读完了,此时 A 拿不到数据就会一直阻塞

因此,为了不阻塞在 channel 上,常见的方法是将“判空与读取”放在一个“事务”中,将“判满与写入”放在一个“事务”中

  • cap
    返回通道的容量大小

4. 与 select 结合使用的一些惯用法

  • 利用 default 分支避免阻塞
    select 语句的 default 分支的语义,就是在其他非 default 分支因通信未就绪,而无法被选择的时候执行的,这就给 default 分支赋予了一种“避免阻塞”的特性。

// $GOROOT/src/time/sleep.go
func sendTime(c interface{}, seq uintptr) {
    // 无阻塞的向c发送当前时间
    select {
    case c.(chan Time) <- Now():
    default:
    }
}
  • 实现超时机制
func worker() {
  select {
  case <-c:
       // ... do some stuff
  case <-time.After(30 *time.Second):
      return
  }
}
  • 实现心跳机制

func worker() {
  heartbeat := time.NewTicker(30 * time.Second)
  defer heartbeat.Stop()
  for {
    select {
    case <-c:
      // ... do some stuff
    case <- heartbeat.C:
      //... do heartbeat stuff
    }
  }
}

三、小结

channel 分为有缓冲的和无缓冲两种,是不同的 goroutine 通信的媒介,可以实现锁机制、消息队列、广播通知,和 select 搭配也可以实现心跳机制、超时机制等,在并发编程中起到了很大的作用!

你可能感兴趣的:(Go大法,golang,开发语言,后端)