Channel

并发模型

并发问题一般有下面这几种:

  • 数据竞争。简单来说就是两个或多个线程同时读写某个变量,造成了预料之外的结果。

  • 原子性。在一个定义好的上下文里,原子性操作不可分割。上下文的定义非常重要。有些代码,你在程序里看起来是原子的,如最简单的 i++,但在机器层面看来,这条语句通常需要几条指令来完成(Load,Incr,Store),不是不可分割的,也就不是原子性的。原子性可以让我们放心地构造并发安全的程序。

  • 内存访问同步。代码中需要控制同时只有一个线程访问的区域称为临界区。Go 语言中一般使用 sync 包里的 Mutex 来完成同步访问控制。锁一般会带来比较大的性能开销,因此一般要考虑加锁的区域是否会频繁进入、锁的粒度如何控制等问题。

  • 死锁。在一个死锁的程序里,每个线程都在等待其他线程,形成了一个首尾相连的尴尬局面,程序无法继续运行下去。

  • 活锁。想象一下,你走在一条小路上,一个人迎面走来。你往左边走,想避开他;他做了相反的事情,他往右边走,结果两个都过不了。之后,两个人又都想从原来自己相反的方向走,还是同样的结果。这就是活锁,看起来都像在工作,但工作进度就是无法前进。

  • 饥饿。并发的线程不能获取它所需要的资源以进行下一步的工作。通常是有一个非常贪婪的线程,长时间占据资源不释放,导致其他线程无法获得资源。

并发与并行

并发是同一时间应对(dealing with)多件事情的能力。逻辑上具备同时处理多个任务的能力;
并行是同一时间动手(doing)做多件事情的能力。是物理上同时执行多个任务。

CSP

用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型
大多数的编程语言的并发编程模型是基于线程和内存同步访问控制,GO借用了 process和channel这两个概念。process是在go语言上的表现就是 goroutine 是实际并发执行的实体和线程类似,每个实体之间是通过channel通讯来实现数据共享,channel 和 mutex (用于内存同步访问控制)类似。

什么是channel

Goroutine 和 channel 是 Go 语言并发编程的 两大基石。Goroutine 用于执行并发任务,channel 用于 goroutine 之间的同步、通信;
Channel 在 gouroutine 间架起了一条管道,在管道里传输数据,实现 gouroutine 间的通信;由于它是线程安全的,所以用起来非常方便;channel 还提供“先进先出”的特性;它还能影响 goroutine 的阻塞和唤醒;
不要通过共享内存来通信,而要通过通信来实现内存共享;

channel原理

对 chan 的发送和接收操作都会在编译期间转换成为底层的发送接收函数。
Channel 分为两种:带缓冲、不带缓冲。对不带缓冲的 channel 进行的操作实际上可以看作“同步模式”,带缓冲的则称为“异步模式”。
同步模式下,发送方和接收方要同步就绪,只有在两者都 ready 的情况下,数据才能在两者间传输(实际上就是内存拷贝)。否则,任意一方先行进行发送或接收操作,都会被挂起,等待另一方的出现才能被唤醒。
异步模式下,在缓冲槽可用的情况下(有剩余容量),发送和接收操作都可以顺利进行。否则,操作的一方(如写入)同样会被挂起,直到出现相反操作(如接收)才会被唤醒。

数据结构

type hchan struct {
	// chan 里元素数量
	qcount   uint
	// chan 底层循环数组的长度
	dataqsiz uint
	// 指向底层循环数组的指针
	// 指针对有缓冲的 channel
	buf      unsafe.Pointer
	// chan 中元素大小
	elemsize uint16
	// chan 是否被关闭的标志
	closed   uint32
	// chan 中元素类型
	elemtype *_type // element type
	// 已发送元素在循环数组中的索引
	sendx    uint   // send index
	// 已接收元素在循环数组中的索引
	recvx    uint   // receive index
	// 等待接收的 goroutine 队列
	recvq    waitq  // list of recv waiters
	// 等待发送的 goroutine 队列
	sendq    waitq  // list of send waiters

	// 保护 hchan 中所有字段
	lock mutex
}

sendqrecvq 分别表示被阻塞的 goroutine,这些 goroutine 由于尝试读取 channel 或向 channel 发送数据而被阻塞。
waitqsudog 的一个双向链表,而 sudog 实际上是对 goroutine 的一个封装:

type waitq struct {
	first *sudog
	last  *sudog
}
type sudog struct {
    g *g

    isSelect bool
    next     *sudog
    prev     *sudog
    elem     unsafe.Pointer // data element (may point to stack)

    acquiretime int64
    releasetime int64
    ticket      uint32
    parent      *sudog // semaRoot binary tree
    waitlink    *sudog // g.waiting list or semaRoot
    waittail    *sudog // semaRoot
    c           *hchan // channel
}

创建

// 无缓冲通道
ch1 := make(chan int)
// 有缓冲通道
ch2 := make(chan int, 10)

最终创建 chan 的函数是 makechan

func makechan(t *chantype, size int64) *hchan

从函数原型来看,创建的 chan 是一个指针。所以我们能在函数间直接传递 channel,而不用传递 channel 的指针。
新建一个 chan 后,内存在堆上分配

接收

接收操作有两种写法

  1. 返回不带 "ok",反应 channel 是否关闭
func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}

2.返回带 "ok"
当接收到相应类型的零值时无法知道是真实的发送者发送过来的值,还是 channel 被关闭后,返回给接收者的默认类型的零值

func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
	_, received = chanrecv(c, elem, true)
	return //通过返回 "received" 这个字段来反应 channel 是否被关闭。
}
l

接收值则比较特殊,会“放到”参数 elem 所指向的地址了,这很像 C/C++ 里的写法。如果代码里忽略了接收值,这里的 elem 为 nil
无论如何,最终转向了 chanrecv 函数

// 位于 src/runtime/chan.go

// chanrecv 函数接收 channel 
//c 的元素并将其写入 ep 所指向的内存地址。
// 如果 ep 是 nil,说明忽略了接收值。
// 如果 block == false,即非阻塞型接收,在没有数据可接收的情况下,返回 (false, false)
// 否则,如果 c 处于关闭状态,将 ep 指向的地址清零,返回 (true, false)
// 否则,用返回值填充 ep 指向的内存地址。返回 (true, true)
// 如果 ep 非空,则应该指向堆或者函数调用者的栈
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	// 省略 debug 内容 …………

	// 如果是一个 nil 的 channel
	if c == nil {
		// 如果不阻塞,直接返回 (false, false)
		if !block {
			return
		}
		// 否则,接收一个 nil 的 channel,goroutine 挂起
		gopark(nil, nil, "chan receive (nil chan)", traceEvGoStop, 2)
		// 不会执行到这里
		throw("unreachable")
	}

	// 在非阻塞模式下,快速检测到失败,不用获取锁,快速返回
	// 当我们观察到 channel 没准备好接收:
	// 1. 非缓冲型,等待发送列队 sendq 里没有 goroutine 在等待
	// 2. 缓冲型,但 buf 里没有元素
	// 之后,又观察到 closed == 0,即 channel 未关闭。
	// 因为 channel 不可能被重复打开,所以前一个观测的时候 channel 也是未关闭的,
	// 因此在这种情况下可以直接宣布接收失败,返回 (false, false)
	if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
		c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
		atomic.Load(&c.closed) == 0 {
		return
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}

	// 加锁
	lock(&c.lock)

	// channel 已关闭,并且循环数组 buf 里没有元素
	// 这里可以处理非缓冲型关闭 和 缓冲型关闭但 buf 无元素的情况
	// 也就是说即使是关闭状态,但在缓冲型的 channel,
	// buf 里有元素的情况下还能接收到元素
	if c.closed != 0 && c.qcount == 0 {
		if raceenabled {
			raceacquire(unsafe.Pointer(c))
		}
		// 解锁
		unlock(&c.lock)
		if ep != nil {
			// 从一个已关闭的 channel 执行接收操作,且未忽略返回值
			// 那么接收的值将是一个该类型的零值
			// typedmemclr 根据类型清理相应地址的内存
			typedmemclr(c.elemtype, ep)
		}
		// 从一个已关闭的 channel 接收,selected 会返回true
		return true, false
	}

	// 等待发送队列里有 goroutine 存在,说明 buf 是满的
	// 这有可能是:
	// 1. 非缓冲型的 channel
	// 2. 缓冲型的 channel,但 buf 满了
	// 针对 1,直接进行内存拷贝(从 sender goroutine -> receiver goroutine)
	// 针对 2,接收到循环数组头部的元素,并将发送者的元素放到循环数组尾部
	if sg := c.sendq.dequeue(); sg != nil {
		// Found a waiting sender. If buffer is size 0, receive value
		// directly from sender. Otherwise, receive from head of queue
		// and add sender's value to the tail of the queue (both map to
		// the same buffer slot because the queue is full).
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}

	// 缓冲型,buf 里有元素,可以正常接收
	if c.qcount > 0 {
		// 直接从循环数组里找到要接收的元素
		qp := chanbuf(c, c.recvx)

		// …………

		// 代码里,没有忽略要接收的值,不是 "<- ch",而是 "val <- ch",ep 指向 val
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
		// 清理掉循环数组里相应位置的值
		typedmemclr(c.elemtype, qp)
		// 接收游标向前移动
		c.recvx++
		// 接收游标归零
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		// buf 数组里的元素个数减 1
		c.qcount--
		// 解锁
		unlock(&c.lock)
		return true, true
	}

	if !block {
		// 非阻塞接收,解锁。selected 返回 false,因为没有接收到值
		unlock(&c.lock)
		return false, false
	}

	// 接下来就是要被阻塞的情况了
	// 构造一个 sudog
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}

	// 待接收数据的地址保存下来
	mysg.elem = ep
	mysg.waitlink = nil
	gp.waiting = mysg
	mysg.g = gp
	mysg.selectdone = nil
	mysg.c = c
	gp.param = nil
	// 进入channel 的等待接收队列
	c.recvq.enqueue(mysg)
	// 将当前 goroutine 挂起
	goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)

	// 被唤醒了,接着从这里继续执行一些扫尾工作
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	closed := gp.param == nil
	gp.param = nil
	mysg.c = nil
	releaseSudog(mysg)
	return true, !closed


发送

ch <- 3

发送操作最终转化为 chansend 函数

// 位于 src/runtime/chan.go

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	// 如果 channel 是 nil
	if c == nil {
		// 不能阻塞,直接返回 false,表示未发送成功
		if !block {
			return false
		}
		// 当前 goroutine 被挂起
		gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2)
		throw("unreachable")
	}

	// 省略 debug 相关……

	// 对于不阻塞的 send,快速检测失败场景
	//
	// 如果 channel 未关闭且 channel 没有多余的缓冲空间。这可能是:
	// 1. channel 是非缓冲型的,且等待接收队列里没有 goroutine
	// 2. channel 是缓冲型的,但循环数组已经装满了元素
	if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
		(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
		return false
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}

	// 锁住 channel,并发安全
	lock(&c.lock)

	// 如果 channel 关闭了
	if c.closed != 0 {
		// 解锁
		unlock(&c.lock)
		// 直接 panic
		panic(plainError("send on closed channel"))
	}

	// 如果接收队列里有 goroutine,直接将要发送的数据拷贝到接收 goroutine
	if sg := c.recvq.dequeue(); sg != nil {
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}

	// 对于缓冲型的 channel,如果还有缓冲空间
	if c.qcount < c.dataqsiz {
		// qp 指向 buf 的 sendx 位置
		qp := chanbuf(c, c.sendx)

		// ……

		// 将数据从 ep 处拷贝到 qp
		typedmemmove(c.elemtype, qp, ep)
		// 发送游标值加 1
		c.sendx++
		// 如果发送游标值等于容量值,游标值归 0
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
		// 缓冲区的元素数量加一
		c.qcount++

		// 解锁
		unlock(&c.lock)
		return true
	}

	// 如果不需要阻塞,则直接返回错误
	if !block {
		unlock(&c.lock)
		return false
	}

	// channel 满了,发送方会被阻塞。接下来会构造一个 sudog

	// 获取当前 goroutine 的指针
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}

	mysg.elem = ep
	mysg.waitlink = nil
	mysg.g = gp
	mysg.selectdone = nil
	mysg.c = c
	gp.waiting = mysg
	gp.param = nil

	// 当前 goroutine 进入发送等待队列
	c.sendq.enqueue(mysg)

	// 当前 goroutine 被挂起
	goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)

	// 从这里开始被唤醒了(channel 有机会可以发送了)
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	if gp.param == nil {
		if c.closed == 0 {
			throw("chansend: spurious wakeup")
		}
		// 被唤醒后,channel 关闭了。坑爹啊,panic
		panic(plainError("send on closed channel"))
	}
	gp.param = nil
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	// 去掉 mysg 上绑定的 channel
	mysg.c = nil
	releaseSudog(mysg)
	return true
}

关闭

关闭某个 channel,会执行函数 closechan

func closechan(c *hchan) {
	// 关闭一个 nil channel,panic
	if c == nil {
		panic(plainError("close of nil channel"))
	}

	// 上锁
	lock(&c.lock)
	// 如果 channel 已经关闭
	if c.closed != 0 {
		unlock(&c.lock)
		// panic
		panic(plainError("close of closed channel"))
	}

	// …………

	// 修改关闭状态
	c.closed = 1

	var glist *g

	// 将 channel 所有等待接收队列的里 sudog 释放
	for {
		// 从接收队列里出队一个 sudog
		sg := c.recvq.dequeue()
		// 出队完毕,跳出循环
		if sg == nil {
			break
		}

		// 如果 elem 不为空,说明此 receiver 未忽略接收数据
		// 给它赋一个相应类型的零值
		if sg.elem != nil {
			typedmemclr(c.elemtype, sg.elem)
			sg.elem = nil
		}
		if sg.releasetime != 0 {
			sg.releasetime = cputicks()
		}
		// 取出 goroutine
		gp := sg.g
		gp.param = nil
		if raceenabled {
			raceacquireg(gp, unsafe.Pointer(c))
		}
		// 相连,形成链表
		gp.schedlink.set(glist)
		glist = gp
	}

	// 将 channel 等待发送队列里的 sudog 释放
	// 如果存在,这些 goroutine 将会 panic
	for {
		// 从发送队列里出队一个 sudog
		sg := c.sendq.dequeue()
		if sg == nil {
			break
		}

		// 发送者会 panic
		sg.elem = nil
		if sg.releasetime != 0 {
			sg.releasetime = cputicks()
		}
		gp := sg.g
		gp.param = nil
		if raceenabled {
			raceacquireg(gp, unsafe.Pointer(c))
		}
		// 形成链表
		gp.schedlink.set(glist)
		glist = gp
	}
	// 解锁
	unlock(&c.lock)

	// Ready all Gs now that we've dropped the channel lock.
	// 遍历链表
	for glist != nil {
		// 取最后一个
		gp := glist
		// 向前走一步,下一个唤醒的 g
		glist = glist.schedlink.ptr()
		gp.schedlink = 0
		// 唤醒相应 goroutine
		goready(gp, 3)
	}
}

close 逻辑比较简单,对于一个 channel,recvq 和 sendq 中分别保存了阻塞的发送者和接收者。关闭 channel 后,对于等待接收者而言,会收到一个相应类型的零值。对于等待发送者,会直接 panic。所以,在不了解 channel 还有没有接收者的情况下,不能贸然关闭 channel。

channel进阶

操作 nil channel closed channel not nil, not closed channel
close panic panic 正常关闭
读 <- ch 阻塞 读到对应类型的零值 阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞
写 ch <- 阻塞 panic 阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞
发生 panic 的情况有三种:
  1. 向一个关闭的 channel 进行写操作;
  2. 关闭一个 nil 的 channel;
  3. 重复关闭一个 channel。

读、写一个 nil channel 都会被阻塞。

发送接收元素的本质

channel 的发送和接收操作本质上都是 “值的拷贝”

package main

import (
	"fmt"
	"time"
)

type user struct {
	name string
	age  int8
}

var u = user{name: "Ankur", age: 25}
var g = &u

func modifyUser(pu *user) {
	fmt.Println("modifyUser Received Vaule", pu)
	pu.name = "Anand"
}

func printUser(u <-chan *user) {
	time.Sleep(2 * time.Second)
	fmt.Println("printUser goRoutine called", <-u)
}

func main() {
	c := make(chan *user, 5)
	c <- g
	fmt.Println(g)
	// modify g
	g = &user{name: "Ankur Anand", age: 100}
	go printUser(c)  //c管道中的值是{Ankur 25}
	go modifyUser(g) //g 是指向user的指针
	//打印不同的值,说明在改变user之前写入管道的user是值拷贝
	time.Sleep(5 * time.Second)
	fmt.Println(g)
}

运行结果:

&{Ankur 25}
modifyUser Received Vaule &{Ankur Anand 100}
printUser goRoutine called &{Ankur 25}
//无缓冲channel
func ch() {
	var ch = make(chan int)
	//无缓冲区,会阻塞等待消费
	go func(ch chan int) {
		ch <- 1
		ch <- 2
		ch <- 3
		fmt.Println("send finished")
	}(ch)

	for {
		select {
		case i := <-ch:
			fmt.Println("receive", i)
		case <-time.After(time.Second):
			fmt.Println("Time out")
			os.Exit(1) //程序退出
			// return
		}
	}
}

//单向只写channel
func chLimit() {
	var ch = make(chan int)

	go func(ch chan<- int) {
		ch <- 1
		ch <- 2
		ch <- 3
		fmt.Println("send finished")
		//invalid operation: cannot receive from send-only channel ch (variable of type chan<- int)
		//r := <-ch
	}(ch)

	for {
		select {
		case i := <-ch:
			fmt.Println("receive", i)
		case <-time.After(time.Second):
			fmt.Println("Time out")
			os.Exit(1) //程序退出
			// return
		}
	}
}

//close channel
func chClose() {
	var ch = make(chan int)

	go func(ch chan<- int) {
		ch <- 1
		ch <- 2
		ch <- 3
		fmt.Println("send finished")
		close(ch)
	}(ch)

	for {
		select {
		case i, ok := <-ch:
			if ok {
				fmt.Println("receive", i)
			} else {
				fmt.Println("channel close")
				os.Exit(0)
			}

		case <-time.After(time.Second):
			fmt.Println("Time out")
			os.Exit(1) //程序退出
		}
	}
}

//关闭ch出现的问题
func chCloseErr() {
	var ch = make(chan int)

	go func(ch chan<- int) {
		ch <- 1
		ch <- 2
		ch <- 3
		fmt.Println("send finished")
		close(ch) //i,ok:=<-ch; 关闭ch,ok返回值为false,i为ch的默认值
	}(ch)

	for {
		select {
		//如果不判断,那么i就会一直得到chan类型的默认值,如int为0,则永远不会停止
		case i := <-ch:
			fmt.Println("receive", i)
		case <-time.After(time.Millisecond * 100): //ch永远会返回默认值,所以定时不会被执行
			fmt.Println("Time out")
			os.Exit(1) //程序退出
		}
	}
}

//异步任务调度
func chTask() {
	var doneCh = make(chan struct{})
	var errCh = make(chan error)

	go func(doneCh chan<- struct{}, errCh chan<- error) {
		if time.Now().Unix()%2 == 0 {
			doneCh <- struct{}{}
		} else {
			errCh <- errors.New("unix time is an odd")
		}
	}(doneCh, errCh)

	select {
	case <-doneCh:
		fmt.Println("done")
	case err := <-errCh:
		fmt.Println("get an error:", err)
	case <-time.After(time.Second):
		fmt.Println("time out")
	}
}

//有缓冲区channel,buf不满
func chBuf() {
	var ch = make(chan int, 3)
	//有缓冲区,相当于一个消息队列,这里不会阻塞,队列满会阻塞
	go func(ch chan int) {
		ch <- 1
		ch <- 2
		ch <- 3
		fmt.Println("send finished") //ch不阻塞,会先打印这句话
	}(ch)

	for {
		select {
		case i := <-ch:
			fmt.Println("receive", i)
		case <-time.After(time.Second):
			fmt.Println("Time out")
			os.Exit(1) //程序退出
			// return
		}
	}
}

//有缓冲区channel,buf满
func chBufs() {
	var ch = make(chan int, 3)
	//有缓冲区,相当于一个消息队列,这里不会阻塞,队列满会阻塞
	go func(ch chan int) {
		ch <- 1
		ch <- 2
		ch <- 3
		ch <- 4
		ch <- 5
		ch <- 6
		ch <- 7
		ch <- 8
		ch <- 9
		ch <- 10
		fmt.Println("send finished") //ch阻塞时,不会执行到这
	}(ch)

	for {
		select {
		case i := <-ch:
			fmt.Println("receive", i)
			time.Sleep(time.Second)
		case <-time.After(time.Second * 10):
			fmt.Println("Time out")
			os.Exit(1) //程序退出
			// return
		}
	}
}

//for range读取ch
func chBufRange() {
	var ch = make(chan int, 3)
	//有缓冲区,相当于一个消息队列,这里不会阻塞,队列满会阻塞
	go func(ch chan int) {
		ch <- 1
		ch <- 2
		ch <- 3
		fmt.Println("send finished") //ch不阻塞,会先打印这句话
		close(ch)
	}(ch)

	//如果不关闭ch或无数据,则会阻塞等待数据
	for i := range ch {
		fmt.Println("receive:", i)
	}
}

//channel  是环型队列,好处是内存重复使用

//ex1
type Ball struct {
	hits int
}

func passBall() {
	table := make(chan *Ball)
	go player("ping", table)
	go player("pong", table)
	//Tip:核心逻辑:往channel里放入数据,作为启动信号;从channel读出数据,作为关闭信号
	table <- new(Ball)
	time.Sleep(time.Second)
	<-table
	close(table)
	time.Sleep(time.Second * 5)
	fmt.Println("passBall exit")
}
func player(name string, table chan *Ball) {
	for ball := range table {
		//Tip:刚进goroutine时,先阻塞在这里
		// ball := <-table
		ball.hits++
		fmt.Println(name, ball.hits)
		time.Sleep(time.Millisecond * 100)
		//Tip:运行到这里时,另一个goroutine在收数据,所以能准确送达
		table <- ball
	}
	fmt.Println("player exit")
}

//ex2,channel嵌套
type sub struct {
	//Tip 把chan error看作一个整体,作为关闭的通道
	closing chan chan error
	updates chan string
}

func (s *sub) Close() error {
	//Tip:核心逻辑:两层通知,第一层作为准备关闭的通知,第二层作为关闭结果的返回
	errc := make(chan error)
	//Tip 第一步:要关闭时,先传一个chan error过去,通知要关闭了
	s.closing <- errc
	//Tip 第三步:从chan error中读取错误,阻塞等待,等待gorutine退出前回复
	return <-errc
}
func (s *sub) loop() {
	var err error
	for {
		select {
		case errc := <-s.closing:
			//Tip 第二步:收到关闭后,进行处理,处理后把error传回去,通知发送关闭的gorutine
			errc <- err
			close(s.updates)
			return
		case s, ok := <-s.updates:
			if ok {
				fmt.Println("update:", s)
			} else {
				fmt.Println("update exit")
				return
			}
		}
	}
}
func m() {
	var s sub = sub{
		closing: make(chan chan error),
		updates: make(chan string),
	}
	go s.loop()
	s.updates <- "1"
	s.updates <- "2"
	s.updates <- "3"
	time.Sleep(time.Second * 2)
	err := s.Close()
	fmt.Println("err:", err)
}
posted @ 2021-10-05 10:14  wangzhilei  阅读(623)  评论(0)    收藏  举报