Bootstrap

【Go channel 不同情况下的系统状况】


前言

最近在学习Golang的时候,注意到channel的传输存在很多种情况,因此在本篇文章中通过实践的方式,整理各种情况的结果,方便自己和大家今后的学习。
channel可以分为3种类型:

//只读 channel,单向 channel
readOlnyChan := make(<-chan int)
只写 channel,单向 channel
writeOlnyChan := make(chan<- int)
可读可写 channel
ch := make(chan)

三个类型种,业务最频繁使用的是可读可写的无限制channel,所以接下来将主要介绍可读可写的channel场景。

各种情况预览


总结下来,异常主要有三个部分。

  1. 超量读写 和 nil读写: 出现了阻塞
  2. close的channel写入: 触发panic
  3. close的读:读出0

Channel的两种使用

Golang中channel的存在使得整个语言在处理异步式或密集IO的问题有着显著的提升,因为Golang的设计,近来的JAVA在jdk21版本设计上也参照Golang的协程理念。

Channel在英文中即管道的意思,在Golang中channel就像管道一样,承担着在多个程序之间传输的任务。针对不同情况,可以创建两种Channel:无缓冲Channel、有缓冲Channel。

无缓冲Channel

创建一种int类型的无缓冲channel,其结果代表协程中处理的最终结果。最终被主线程接受

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)

	go func() {
		// 执行任务
		fmt.Println("执行协程任务开始")
		time.Sleep(1 * time.Second)
		fmt.Println("执行协程任务结束")
		// 假设得到结果 为1
		// 传1到channel中
		ch <- 1
	}()

	// 主线程异步执行其他任务
	fmt.Println("执行主线程任务开始")
	time.Sleep(2 * time.Second) //假设执行需要2s
	fmt.Println("执行主线程任务结束")

	select {
	case x := <-ch:
		fmt.Println("接受Channel", x)
	case <-time.After(time.Second):
		fmt.Println("timeout")
	}

}


可以看到主线程任务和协程任务,两者同时开始,并且在最后将channel中的值传给了主线程。这两者在执行任务的过程是同步。当然还有更多中关于无缓冲Channel的使用,这里就不多介绍。

有缓冲Channel的使用

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 3)

	// 1. 正常使用buffer
	// 结果制造程序
	go func() {
		// 程序需执行三次计算,每次计算耗时1s
		for i := 0; i < 3; i++ {
			time.Sleep(1 * time.Second)
			fmt.Println("任务结束,传输的值为:", i)
			ch <- i
			fmt.Println("传输结果:", i)
		}
	}()

	// 结果接受程序
	for i := 0; i < 3; i++ {
		select {
		case v := <-ch:
			fmt.Println("获取结果:", v)
		case <-time.After(3 * time.Second):
			fmt.Println("正常读写出现阻塞")
		}
	}
}

通过make(chan int 3)来创建一个int类型的有缓冲buffer,其中3表示缓冲的长度。上述代码运行结果如下。

上述就简单的演示一下有缓冲channel的一种使用情况。


无缓冲Channel的所有情况

无缓冲Channel在使用中主要可能出现5种读写场景。

正常读写

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)

	// 1. 正常channel读写
	fmt.Println("----------正常channel读写-----------------")
	go func() {
		ch <- "1. 正常读写"
	}()
	select {
	case data := <-ch:
		fmt.Println(data, "读写成功")
	case <-time.After(time.Second):
		fmt.Println("1. 正常读写:1s后读写阻塞了。。。")
	}
}

标准的无缓冲channel读写

连续写入

// 2.连续写
	ch = make(chan string)
	flag := make(chan bool)
	fmt.Println("----------连续写-----------------")
	go func() {
		for i := 0; i < 3; i++ {
			fmt.Printf("第%d次写入\n", i+1)
			ch <- "这是第" + string(rune(i)) + "次"
		}
		flag <- true
		// 循环是否出来
		fmt.Println("循环结束")

	}()
	select {
	case <-flag:
		fmt.Println("没有阻塞")
	case <-time.After(time.Second):
		fmt.Println("2.连续写:阻塞了")
	}


如果无缓冲channel中不是空的,那么下次写入就会阻塞,直到有程序将其读出

package main

import (
	"fmt"
	"time"
)

func main() {
	// 3. 连续读
	fmt.Println("----------连续读-----------------")
	// 创建一个空channel
	ch := make(chan string)
	flag := make(chan bool)
	go func() {
		// 持续读
		for i := 0; i < 3; i++ {
			fmt.Printf("第%d次读\n", i+1)
			x := <-ch
			fmt.Println("ch的值为:", x)
		}
		flag <- true
	}()
	select {
	case <-flag:
		fmt.Println("没有阻塞")
	case <-time.After(time.Second):
		fmt.Println("3. 连续读:阻塞了")
	}
}

类似的,如果无缓冲buffer中没有数据,那么读也会被阻塞,直到channel中出现写入。

nil的channel

写入情况

package main

import (
	"fmt"
	"time"
)

func main() {
	var ch chan string = nil
	// 4. nil的channel读写
	fmt.Println("----------nil的channel读写-----------------")
	go func() {
		ch <- "4. nil的channel读写"
		fmt.Println("写入完成")
	}()

	select {
	case data := <-ch:
		fmt.Println(data, "读写成功")
	case <-time.After(time.Second):
		fmt.Println("4. 正常读写:1s后读写阻塞了。。。")
	}
}

可以看到,协程中的写入操作 = 没有写入。并且fmt.Println("写入完成")部分也没有执行,说明写入部分出现了阻塞,主线程在通过select的操作时并没有检查到ch中的输入信号,所以最终倒计时结束执行退出操作。

读取状况

package main

import (
	"fmt"
	"time"
)

func main() {
	var ch chan int = nil
	go func() {
		fmt.Println(<-ch)
		fmt.Println("读取成功")
	}()

	time.Sleep(1 * time.Second)
}

运行结果也并没出现读取成功的字样,所以协程中也出现了阻塞的情况。

已关闭的channel

写入情况

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)
	close(ch)
	// 5. channel关闭情况下读写
	fmt.Println("----------channel关闭情况下读写-----------------")
	go func() {
		ch <- "5. close的channel读写"
	}()
	time.Sleep(1 * time.Second)
}

可以看到在向关闭的channel写入时,会触发panic,从而终止程序。

读取情况

import "fmt"

func main() {
	ch := make(chan int)
	close(ch)
	fmt.Println("读取channel")
	x := <-ch
	fmt.Println(x)
}


代码结果表明,在读取closechannel时,会返回一个0作为结果,而不会像无值的时候那样出现阻塞。

总结

将上述的情况进行总结,可以得到有缓冲channel在多种情况下,在每个程序中会带来的影响。

主要有两个特殊情况,即close读写的两个特殊情况。

  1. close的channel写会造成panic
  2. close的channel读在int类型下会读到0。

有缓冲Channel的所有情况

Channel中还有有缓冲的设计模式,通过在创建时,对channel设定一定的大小的长度,来表示channel最大能缓冲的数据量。

正常读写

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 3)
	fmt.Println("-----------正常读写---------------")
	// 1. 正常使用buffer
	// 结果制造程序
	go func() {
		// 程序需执行三次计算,每次计算耗时1s
		for i := 0; i < 3; i++ {
			time.Sleep(1 * time.Second)
			fmt.Println("任务结束,传输的值为:", i)
			ch <- i
			fmt.Println("传输结果:", i)
		}
	}()

	// 结果接受程序
	for i := 0; i < 3; i++ {
		select {
		case v := <-ch:
			fmt.Println("获取结果:", v)
		case <-time.After(3 * time.Second):
			fmt.Println("正常读写出现阻塞")
		}
	}

}

代码中一个协程进行数据的筛入,主线程进行获取。最终很好的实现三次传输和接受的同步。并且整个程序也没有出现阻塞的现象。

超量写不读

package main

import (
	"fmt"
	"time"
)

func main() {
	// 2. 超量写
	fmt.Println("----------超量写-----------")
	ch := make(chan int, 3)
	flag := make(chan bool)
	go func() {
		for i := 0; i < 5; i++ {
			time.Sleep(100 * time.Millisecond)
			fmt.Println("程序结束,传输数据", i)
			ch <- i
			fmt.Println("传输成功,", i)
		}
		fmt.Println("数据传输完毕")
		// 没有阻塞
		flag <- true
	}()

	select {
	case <-flag:
		fmt.Println("没有阻塞")
	case <-time.After(6 * time.Second):
		fmt.Println("6s 后发现阻塞")
	}
}

可以看到只写不读的情况下,当传入channel循环到第三个数据时,就不会传输了,此协程中就出现了阻塞。最终在主线程中结束了等待。

超量读不写

package main

import (
	"fmt"
	"time"
)

func main() {
	// 3. 超量读
	fmt.Println("----------超量读-----------")
	// 重新创建新的
	ch := make(chan int, 3)
	flag := make(chan bool)
	go func() {
		for i := 0; i < 5; i++ {
			fmt.Println("程序计算完毕,准备获取channel内部的值")
			v := <-ch
			fmt.Println("此时无值,但要超量读,v的值为:", v)
		}
		fmt.Println("channel数据获取完毕")
		flag <- true
	}()

	select {
	case <-flag:
		fmt.Println("没有阻塞")
	case <-time.After(6 * time.Second):
		fmt.Println("6s 后发现阻塞")
	}
}

可以看到,在协程中channel根本就没有数据,所以自然而然就发生了阻塞,进而协程中的程序将不再执行,直到主线程等待6s后结束程序。

nil 的有缓冲channel的情况

package main

import (
	"fmt"
	"time"
)

func main() {
	var ch chan []int = nil
	flag := make(chan bool)
	go func() {
		ch <- []int{1, 2, 3}

		flag <- true
		fmt.Println("数据写入成功")
	}()

	select {
	case <-flag:
		fmt.Println("channel数据写入成功")
	case <-time.After(time.Second):
		fmt.Println("1s后出现阻塞")
	}
}


可以看到在新建的协程中,执行ch <- []int{1, 2, 3}时出现了阻塞,协程无法继续向后执行,flag无法接受到结束信号,使的整个协程出现了阻塞。直到主线程中select将整个进程按阻塞情况处理了。

package main

import (
	"fmt"
	"time"
)

func main() {
	var ch chan []int = nil
	flag := make(chan bool)
	go func() {
		x := <-ch
		fmt.Println("x:", x)
		flag <- true
		fmt.Println("数据写入成功")
	}()

	select {
	case <-flag:
		fmt.Println("程序结束")
	case <-time.After(1 * time.Second):
		fmt.Println("程序阻塞")
	}
}

结果也是出现了阻塞,当然这也是符合预期的。

关闭缓冲Channel的所有情况

正常读写

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int, 3)
	close(ch)
	go func() {
		for i := 0; i < 3; i++ {
			ch <- i
		}
		fmt.Println("数据传输成功")
	}()

	for i := 0; i < 3; i++ {
		select {
		case v := <-ch:
			fmt.Println("收到数据,", v)
		case <-time.After(time.Second):
			fmt.Println("阻塞")
		}
	}
}

当执行上述的关闭程序时,发现出现了收到了数据和send on closed channel的错误,猜测写入关闭channel会出现panic,读的时候会读到0。也就是说有缓冲和无缓冲的channel的关闭情况是相同的。

总结

结果可以看到,有缓冲的channel的各种情况的和无缓冲的channel相类似。

最后总结

可以看到,不管是无缓冲和有缓冲channel,两者的机理相类似,这必然是golang中内部源码提前设计好的,如果大家喜欢的话,之后可以再深入探讨一下^-^

总结下来,异常主要有三个部分。

  1. 超量读写 和 nil读写: 出现了阻塞
  2. close的channel写入: 触发panic
  3. close的读:读出0

本文是经过个人查阅相关资料后理解的提炼,可能存在理论上理解偏差的问题,如果您在阅读过程中发现任何问题或有任何疑问,请不吝指出,我将非常感激并乐意与您讨论。谢谢您的阅读!

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;