侧边栏壁纸
博主头像
SeaDream乄造梦

Dream,Don't stop a day of hard and don't give up a little hope。 ——不停止一日努力&&不放弃一点希望。

  • 累计撰写 67 篇文章
  • 累计创建 21 个标签
  • 累计收到 14 条评论

目 录CONTENT

文章目录

go 关键字select

SeaDream乄造梦
2024-12-10 / 0 评论 / 0 点赞 / 86 阅读 / 3,722 字
温馨提示:
亲爱的,如果觉得博主很有趣就留下你的足迹,并收藏下链接在走叭

elect是Golang在语言层面提供的多路IO复用的机制,其可以检测多个channel是否ready(即是否可读或可写),使用起来非常方便。

  • select 执行顺序是随机的
  • select会按照随机的顺序检测各case语句中channel是否ready,如果某个case中的channel已经ready则执行相应的case语句然后退出select流程,如果所有的channel都未ready且没有default的话,则会阻塞等待各个channel

代码示例


func addNumberToChan(chanName chan int) {
	for {
		chanName <- 1
		time.Sleep(1 * time.Second)
	}
}

func TestSelect(t *testing.T) {
	var chan1 = make(chan int, 10)
	var chan2 = make(chan int, 10)
	go addNumberToChan(chan1)
	go addNumberToChan(chan2)
	for {
		select {
		case e := <-chan1:
			fmt.Printf("Get element from chan1: %d\n", e)
		case e := <-chan2:
			fmt.Printf("Get element from chan2: %d\n", e)
			//default:
			//	fmt.Printf("No element in chan1 and chan2.\n")
			//	time.Sleep(1 * time.Second)
		}
	}
}

func TestSelect1(t *testing.T) {
	testSelect1()
}

func testSelect1() {
	chan1 := make(chan int)
	chan2 := make(chan int)

	go func() {
		chan1 <- 1
		time.Sleep(5 * time.Second)
	}()

	go func() {
		chan2 <- 1
		time.Sleep(5 * time.Second)
	}()

	select {
	case <-chan1:
		fmt.Println("chan1 ready.")
	case <-chan2:
		fmt.Println("chan2 ready.")
	default:
		fmt.Println("default")
	}

	fmt.Println("main exit.")
}

func TestSelect2(t *testing.T) {
	testSelect2()
}

func testSelect2() {
	chan1 := make(chan int)
	chan2 := make(chan int)

	writeFlag := false
	go func() {
		for {
			if writeFlag {
				chan1 <- 1
			}
			time.Sleep(time.Second)
		}
	}()

	go func() {
		for {
			if writeFlag {
				chan2 <- 1
			}
			time.Sleep(time.Second)
		}
	}()

	select {
	case <-chan1:
		fmt.Println("chan1 ready.")
	case <-chan2:
		fmt.Println("chan2 ready.")
	}

	fmt.Println("main exit.")
}

func TestSelect3(t *testing.T) {
	testSelect3()
}

func testSelect3() {
	chan1 := make(chan int)
	chan2 := make(chan int)

	go func() {
		close(chan1)
	}()

	go func() {
		close(chan2)
	}()

	select {
	case <-chan1:
		fmt.Println("chan1 ready.")
	case <-chan2:
		fmt.Println("chan2 ready.")
	}

	fmt.Println("main exit.")

}

func TestSelect4(t *testing.T) {
	select {}
}

// 对于空的select语句,程序会被阻塞,准确的说是当前协程被阻塞,同时Golang自带死锁检测机制,当 发现当前协程再也没有机会被唤醒时,则会panic。所以上述程序会panic。


道关闭时的行为

在 Go 中,当一个通道被关闭后:

  1. 读取操作不会阻塞:当你从一个关闭的通道中读取数据时:
  • 如果通道中有未被读取的值,读取这些值并继续。
  • 如果通道中已经没有值,则会读取到通道类型的“零值”(对于 int 类型的通道,零值是 0)。
  • 此外,还会返回一个布尔值(在直接使用 <-chan 时被忽略),用于指示通道是否已关闭。这个布尔值为 false 时,表示通道已关闭。
  1. 写入操作会触发 panic:尝试向一个已经关闭的通道写入数据会导致程序崩溃。

case数据结构

 type scase struct { 
	 c *hchan // chan 
	 kind uint16 
	 elem unsafe.Pointer // data element 
 }

scase.c为当前case语句所操作的channel指针,这也说明了一个case语句只能操作一个channel。scase.kind表示该case的类型,分为读channel、写channel和default,三种类型分别由常量定义:

  • caseRecv:case语句中尝试读取scase.c中的数据;
  • caseSend:case语句中尝试向scase.c中写入数据;
  • caseDefault: default语句

scase.elem表示缓冲区地址,根据scase.kind不同,有不同的用途:

  • scase.kind == caseRecv : scase.elem表示读出channel的数据存放地址;
  • scase.kind == caseSend : scase.elem表示将要写入channel的数据存放地址;

select实现逻辑

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)

函数参数:

  • cas0为scase数组的首地址,selectgo()就是从这些scase中找出一个返回。
  • order0为一个两倍cas0数组长度的buffer,保存scase随机序列pollorder和scase中channel地址序列
    lockorder
    • pollorder:每次selectgo执行都会把scase序列打乱,以达到随机检测case的目的。
    • lockorder:所有case语句中channel序列,以达到去重防止对channel加锁时重复加锁的目的。
  • ncases表示scase数组的长度

函数返回值:

  1. int: 选中case的编号,这个case编号跟代码一致
  2. bool: 是否成功从channle中读取了数据,如果选中的case是从channel中读数据,则该返回值表示是否读取成功。

selectgo实现伪代码如下:

1. func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) { 
2. //1. 锁定scase语句中所有的channel 
3. //2. 按照随机顺序检测scase中的channel是否ready 
4. // 2.1 如果case可读,则读取channel中数据,解锁所有的channel,然后返回(case index, true) 
5. // 2.2 如果case可写,则将数据写入channel,解锁所有的channel,然后返回(case index, false) 
6. // 2.3 所有case都未ready,则解锁所有的channel,然后返回(default index, false) 
7. //3. 所有case都未ready,且没有default语句 
8. // 3.1 将当前协程加入到所有channel的等待队列 
9. // 3.2 当将协程转入阻塞,等待被唤醒 
10. //4. 唤醒后返回channel对应的case index 
11. // 4.1 如果是读操作,解锁所有的channel,然后返回(case index, true) 
12. // 4.2 如果是写操作,解锁所有的channel,然后返回(case index, false) 
13. }

特别说明:对于读channel的case来说,如 case elem, ok := <-chan1: , 如果channel有可能被其他协程关闭的情况下,一定要检测读取是否成功,因为close的channel也有可能返回,此时ok == false。

总结

  • select语句中除default外,每个case操作一个channel,要么读要么写
  • select语句中除default外,各case执行顺序是随机的
  • select语句中如果没有default语句,则会阻塞等待任一case
  • select语句中读操作要判断是否成功读取,关闭的channel也可以读取
0

评论区