Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Golang Channel

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

作者:Ted刘

Channel是一个提供可接收和发送特定类型值的用于并发函数通信的数据类型,也是Golang并发绕不开的重要组件之一,本文就来和大家深入聊聊Channel的相关知识吧

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

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

Channel使用

语法

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

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

chan为channel类型关键字

<- 操作符用于channel中数据的收发,在声明时用于表示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)

如果不使用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中的元素值
}

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

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

channel的关闭

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

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

channel分类

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

无缓冲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,是典型的队列操作:

操作示例:

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进行操作。

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

生命周期

创建策略

发送策略

接收策略

关闭

调用 runtime.closechan 函数

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

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

您可能感兴趣的文章:
阅读全文