Golang并发绕不开的重要组件之Channel详解

 更新时间:2023年06月04日 16:04:59   作者:Ted刘  
Channel是一个提供可接收和发送特定类型值的用于并发函数通信的数据类型,也是Golang并发绕不开的重要组件之一,本文就来和大家深入聊聊Channel的相关知识吧

在上一篇文章中有介绍Golang实现并发的重要关键字 go,通过这个我们可以方便快速地启动Goroutinue协程。协程之间一定会有通信的需求,而Golang的核心设计思想为:不通过共享内存的方式进行通信,而应该通过通信来共享内存。与其他通过共享内存来进行数据传递的编程语言略有差异,而实现这一方案的正是 Channel。

Channel是一个提供可接收和发送特定类型值的用于并发函数通信的数据类型,满足FIFO(先进先出)原则的队列类型。FIFO在数据类型与操作上都有体现:

  • Channel类型的元素是先进先出的,先发送到Channel的元素会先被接收
  • 先向channel发送数据的Goroutinue会优先执行
  • 先从channel接收数据的Goroutinue会优先执行

Channel使用

语法

channel是Golang中的一种数据类型,相关语法也非常简单

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType 

chan为channel类型关键字

<- 操作符用于channel中数据的收发,在声明时用于表示channel数据流动的方向

  • chan 默认为双向传递,即channel既可以接收数据也可以发送数据
  • chan<- 仅可以发送数据的channel
  • <-chan 仅可以接受数据的channel

ElementType 代表元素类型,例如 int、string...

初始化

channel数据类型是一种引用类型,类似于map和slice,所以channel的初始化需要使用内建函数make():

make(ChannelType, Capacity)

ch := make(chan int) 
var ch = make(chan int) 
ch := make(chan int, 10) 
ch := make(<-chan int) 
ch := make(chan<- int, 10)
  • ChannelType就是前面介绍的类型
  • Capacity代表缓冲容量。省略时就是为默认0,表示无缓冲的Channel

如果不使用make()函数来初始化channel,则不能执行收发通信操作,并且会造成阻塞,进而造成Goroutinue泄露,示例:

func main() {
	defer func() {
		fmt.Println("goroutines: ", runtime.NumGoroutine())
	}()
	var ch chan int
	go func() {
		<-ch
	}()
	time.Sleep(time.Second)
}

代码执行结果为:

goroutines:  2

可以看到,直到程序退出,Goroutinue数量仍然为2,原因就是channel没有正确的使用make()进行初始化,channel变量实际为nil,进而造成了内存泄露。

数据的接收与发送

channel中数据的接收与发送是通过操作符 <- 来进行操作的:

// 接收数据
ch <- Expression
ch <- 111
ch <- struct{}{}
// 发送数据
<- ch
v := <- ch
f(<-ch)

除了操作符 <- 外,我们还可以使用 for range 持续地从channel中接收数据:

for e := range ch {
    // e逐个读取ch中的元素值
}

持续接收操作与 <- 没有很大区别:

  • 如果ch为nil channel就会阻塞
  • 如果ch没有发送元素也会阻塞

for 会持续读取直到channel执行关闭,关闭后for会将剩余元素全部读取之后结束。那么对已经关闭的channel进行数据的收发会怎样呢?

channel的关闭

channel使用过后要使用内置函数close()来关闭channel。关闭channel的意思是记录该Channel不能再被发送任何元素了,而不是销毁该Channel的意思。也就意味着关闭的Channel是可以继续接收值的

  • 如果向已经关闭的channel发送数据会引发panic
  • 关闭 nil channel 会引发panic
  • 关闭已经关闭的 channel 会引发panic
  • 如果读取已经关闭的channel值,可以接收关闭前发送的全部值;关闭前的值接收完会返回类型的零值和一个false,不会阻塞及panic

以上几种情况可以自己编写一个简单的代码来测试一下。

channel分类

前面有提到,在make一个channel时,第二个参数就代表了缓冲区大小,如果没有第二个参数就为默认的无缓冲channel。具体用法:

  • 缓冲Channel,make(chan T, cap),cap是大于0的值。
  • 无缓冲Channel, make(chan T), make(chan T, 0)

无缓冲channel

无缓冲的channel也称为同步Channel,只有当发送方和接收方都准备就绪时,通信才会成功。

同步操作示例:

func ChannelSync() {
// 初始化数据
ch := make(chan int)
wg := sync.WaitGroup{}
    // 间隔发送
    wg.Add(1)
    go func() {
        defer wg.Done()
        for i := 0; i < 5; i++ {
            ch <- i
            println("Send ", i, ".\tNow:", time.Now().Format("15:04:05.999999999"))
            // 间隔时间
            time.Sleep(1 * time.Second)
        }
        close(ch)
    }()
    // 间隔接收
    wg.Add(1)
    go func() {
        defer wg.Done()
        for v := range ch {
            println("Received ", v, ".\tNow:", time.Now().Format("15:04:05.999999999"))
            // 间隔时间,注意与send的间隔时间不同
            time.Sleep(3 * time.Second)
        }
    }()
    wg.Wait()
}

执行结果:

Send  0 .       Now: 17:54:27.772773
Received  0 .   Now: 17:54:27.772795
Received  1 .   Now: 17:54:30.773878
Send  1 .       Now: 17:54:30.773959
Received  2 .   Now: 17:54:33.775132
Send  2 .       Now: 17:54:33.775208
Received  3 .   Now: 17:54:36.775816
Send  3 .       Now: 17:54:36.775902
Received  4 .   Now: 17:54:39.776408
Send  4 .       Now: 17:54:39.776456

代码中,采用同步channel,使用两个goroutine完成发送和接收。每次发送和接收的时间间隔不同。我们分别打印发送和接收的值和时间。可以看到执行结果:发送和接收时间一致;间隔以长的为准,可见发送和接收操作为同步操作。因此,同步Channel适合在gotoutine间用做同步的信号

缓冲Channel

缓冲Channel也称为异步Channel,接收和发送方不用等待双方就绪即可成功。缓冲Channel会存在一个容量为cap的缓冲空间。当使用缓冲Channel通信时,接收和发送操作是在操作Channel的Buffer,是典型的队列操作:

  • 接收时,从缓冲中接收元素,只要缓冲不为空,不会阻塞。反之,缓冲为空,会阻塞,goroutine挂起
  • 发送时,向缓冲中发送元素,只要缓冲未满,不会阻塞。反之,缓冲满了,会阻塞,goroutine挂起

操作示例:

func main() {
	// 初始化数据
	ch := make(chan int, 5)
	wg := sync.WaitGroup{}
	// 间隔发送
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 5; i++ {
			ch <- i
			println("Send ", i, ".\tNow:", time.Now().Format("15:04:05.999999999"))
			// 间隔时间
			time.Sleep(1 * time.Second)
		}
	}()
	// 间隔接收
	wg.Add(1)
	go func() {
		defer wg.Done()
		for v := range ch {
			println("Received ", v, ".\tNow:", time.Now().Format("15:04:05.999999999"))
			// 间隔时间,注意与send的间隔时间不同
			time.Sleep(3 * time.Second)
		}
	}()
	wg.Wait()
}

执行结果:

Send  0 .       Now: 17:59:32.990698
Received  0 .   Now: 17:59:32.99071
Send  1 .       Now: 17:59:33.992127
Send  2 .       Now: 17:59:34.992832
Received  1 .   Now: 17:59:35.991488
Send  3 .       Now: 17:59:35.993155
Send  4 .       Now: 17:59:36.993445
Received  2 .   Now: 17:59:38.991663
Received  3 .   Now: 17:59:41.99184
Received  4 .   Now: 17:59:44.992214

代码中,与同步channel一致,只是采用了容量为5的缓冲channel,使用两个goroutine完成发送和接收。每次发送和接收的时间间隔不同。我们分别打印发送和接收的值和时间。可以看到执行结果:发送和接收时间不同;发送和接收操作不会阻塞,可见发送和接收操作为异步操作。因此,缓冲channel非常适合做goroutine之间的数据通信

Channel原理

源码

在源码包中的 runtime/chan.go 可以看到Channel实现源码:

type hchan struct {
    qcount   uint           // 元素个数,通过len()获取
    dataqsiz uint           // 缓冲队列的长度,即容量,通过cap()获取
    buf      unsafe.Pointer // 缓冲队列指针,无缓冲队列则为nil
    elemsize uint16         // 元素大小
    closed   uint32         // 关闭标志
    elemtype \*\_type // 元素类型
    sendx    uint   // 发送元素索引
    recvx    uint   // 接收元素索引
    recvq    waitq  // 接收Goroutinue队列
    sendq    waitq  // 发送Goroutinue队列
        // lock protects all fields in hchan, as well as several
        // fields in sudogs blocked on this channel.
        //
        // Do not change another G's status while holding this lock
        // (in particular, do not ready a G), as this can deadlock
        // with stack shrinking.
    lock mutex // 锁
}

buf 可以理解为一个环形数组,用来缓存Channel中的元素。为何使用环形数组而不使用普通数组呢?因为普通数组更适合指定的空间,弹出元素时,普通数组需要全部都前移,而使用环形数组+下标索引的方式可以在不移动元素的情况下实现数据的高效读写。

sendx与recvx 当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置。

recvq与sendq 用于记录等待接收和发送的goroutine队列,当基于某channel的接收或发送的goroutine无法理解执行时,也就是需要阻塞时,会被记录到Channel的等待队列中。当channel可以完成相应的接收或发送操作时,从等待队列中唤醒goroutine进行操作。

等待队列实际是一个双向链表结构

生命周期

创建策略

  • 无缓冲的直接分配内存
  • 有缓冲的不包含指针,为hchan和底层数组分配连续的地址
  • 有缓冲的channel且包含元素指针,会为hchan和底层数组分配地址

发送策略

  • 发送操作编译时转换为 runtime.chansend函数
  • 阻塞式:block=true;非阻塞式:block=false
  • 向channel中发送数据分为检查和数据发送两块,数据发送:
    • 如果channel的读等待队列存在接受者goroutinue
      • 将数据直接发送给第一个等待的goroutinue,唤醒接收的goroutinue
    • 如果channel读等待队列不存在接收者goroutinue
      • 如果循环数组buf未满,则将数据发送到循环数组buf的队尾
      • 如果循环数组buf已满,这时就会走阻塞发送的流程,将当前goroutinue加入写等待队列,并挂起等待唤醒

接收策略

  • 接收操作编译是转换为 runtime.chanrecv 函数
  • 阻塞式:block=true;非阻塞式:block=false
  • 向channel中接收数据数据接收:
    • 如果channel的写等待队列存在发送者goroutinue:
      • 如果是无缓冲channel,直接从第一个发送者goroutinue将数据拷贝给接收变量,唤醒发送的goroutinue
      • 如果是有缓冲channel(已满),将循环数组buf的队首元素拷贝给接收变量,将第一个发送者goroutinue的数据拷贝到buf循环数组,唤醒发送的goroutinue
    • 如果channel的写等待不存在发送者goroutinue
      • 如果循环数组buf非空,将循环数组buf的队首元素拷贝给接收变量
      • 如果循环数组buf为空,这个时候就会走阻塞接收的流程,将当前 goroutine 加入读等待队列,并挂起等待唤醒

关闭

调用 runtime.closechan 函数

简单的对Channel一些基础用法及原理做了一个解释,可以多写一写并发代码以及阅读源码来加深对Channel的理解。

以上就是Golang并发绕不开的重要组件之Channel详解的详细内容,更多关于Golang Channel的资料请关注脚本之家其它相关文章!

相关文章

  • Go 库性能分析工具pprof

    Go 库性能分析工具pprof

    这篇文章主要为大家介绍了Go 库性能分析工具pprof,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • 一文带你了解Go语言中的I/O接口设计

    一文带你了解Go语言中的I/O接口设计

    I/O 操作在编程中扮演着至关重要的角色,它涉及程序与外部世界之间的数据交换,下面我们就来简单了解一下Go语言中的 I/O 接口设计吧
    2023-06-06
  • 解决golang http.FileServer 遇到的坑

    解决golang http.FileServer 遇到的坑

    这篇文章主要介绍了解决golang http.FileServer 遇到的坑,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 如何go语言比较两个对象是否深度相同

    如何go语言比较两个对象是否深度相同

    这篇文章主要介绍了如何go语言比较两个对象是否深度相同,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-05-05
  • Go语言中一些不常见的命令参数详解

    Go语言中一些不常见的命令参数详解

    这篇文章主要给大家介绍了关于Go语言中一些不常见的命令参数的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2017-12-12
  • PHP和GO对接ChatGPT实现聊天机器人效果实例

    PHP和GO对接ChatGPT实现聊天机器人效果实例

    这篇文章主要为大家介绍了PHP和GO对接ChatGPT实现聊天机器人效果实例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01
  • Golang中的关键字(defer、:=、go func())详细解读

    Golang中的关键字(defer、:=、go func())详细解读

    这篇文章主要介绍了Golang中的关键字(defer、:=、go func())详细解读,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-04-04
  • Go并发与锁的两种方式该如何提效详解

    Go并发与锁的两种方式该如何提效详解

    如果没有锁,在我们的项目中,可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态),下面这篇文章主要给大家介绍了关于Go并发与锁的两种方式该如何提效的相关资料,需要的朋友可以参考下
    2022-12-12
  • Golang使用lua脚本实现redis原子操作

    Golang使用lua脚本实现redis原子操作

    这篇文章主要介绍了Golang使用lua脚本实现redis原子操作,本文通过实例代码给大家介绍的非常详细,对大家的工作或学习具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03
  • golang 实现一个restful微服务的操作

    golang 实现一个restful微服务的操作

    这篇文章主要介绍了golang 实现一个restful微服务的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04

最新评论