聊一聊Go中channel的行为

简介

当我第一次开始使用Go的channel的时候,我犯了一个错误,认为channel是一个数据结构。我将channel看做是在goroutine之间提供自动同步访问的队列。这种结构上的理解使我编写了许多糟糕而复杂的并发代码。算法

随着时间的推移,我逐渐了解到,最好的办法是忘掉channel的结构,关注它们的行为。因此提到channel,我想到了一个概念:信号。一个通道容许一个goroutine向另外一个goroutine发出特定事件的信号。信号是应该使用channel作的一切的核心。将channel看做一种信号机制,可让你编写具备明肯定义和更精确的行为的代码。数据结构

要理解信号是如何工做的,咱们必须理解它的三个属性:并发

  • 交付保证
  • 状态
  • 有数据或无数据

这三个属性共同构成了围绕信号的设计理念。 在讨论这些属性以后,咱们将提供一些代码示例,这些示例演示如何使用这些属性进行信号传递。函数

交付保证

交付保证是基于一个问题:“我是否须要保证由特定的goroutine发送的信号已经被收到了?”spa

换句话说,咱们能够给出下面这个示例:设计

01 go func() {
02     p := <-ch // 接收
03 }()
04
05 ch <- "paper" // 发送

执行发送的goroutine是否须要保证,经过第5行被发送的一份报告(paper),在继续执行以前,被第2行要接收的goroutine接收到了?code

根据这个问题的答案,你会知道使用两种channel中的哪种:无缓冲或缓冲。每种channel在交付保证时提供不一样的行为。队列

保证是很重要的。好比,若是你没有生活保证时,你不会紧张吗?在编写并发软件时,对是否须要保证有一个重要的理解是相当重要的。随着咱们的继续,你将学会如何作出决定。事件

状态

channel的行为直接受其当前状态的影响。channel的状态能够为nilopenclosed同步

如下示例将介绍,如何在这三个状态中声明或设置一个channel。

// ** nil channel

// 若是声明为零值的话,将会是nil状态
var ch chan string

// 显式的赋值为nil,设置为nil状态
ch = nil


// ** open channel

// 使用内部函数make建立的channel,为open状态
ch := make(chan string)    


// ** closed channel

// 使用close函数的,为closed状态
close(ch)

状态决定了发送接收操做的行为。

信号经过一个channel发送和接收。不能够称为读/写,由于channel不执行输入/输出。

当一个channel为nil状态时,channel上的任何发送或接收都将被阻塞。当为open状态时,信号能够被发送和接收。若是被置为closed状态的话,信号不能再被发送,但仍有可能接收到信号。

有数据或无数据

须要考虑的最后一个信号属性是,信号是否带有数据。

经过在channel上执行发送带有数据的信号。

01 ch <- "paper"

当你用数据发出信号时,一般是由于:

  • goroutine被要求开始一项新任务。
  • goroutine报告了一个结果。

经过关闭一个channel来发送没有数据的信号。

01 close(ch)

当发送没有数据信号的时候,一般是由于:

  • goroutine被告知要中止他们正在作的事情。
  • goroutine报告说已经完成,没有结果。
  • goroutine报告说它已经完成了处理,而且关闭。

没有数据的信号传递的一个好处是,一个单一的goroutine能够同时发出不少的信号。而在goroutines之间,用数据发送信号一般是一对一之间的交换。

有数据信号

当要使用数据进行信号传输时,您能够根据须要的担保类型选择三种channel配置选项。

这三个channel选项是无缓冲,缓冲>1或缓冲=1。

  • 有保证
    • 一个没有缓冲的通道能够保证发送的信号已经收到。
      • 由于信号的接收在信号发送完成以前就发生了。
  • 无保证
    • 一个大小>1的缓冲通道不能保证发送的信号已经收到。
      • 由于信号的发送是在信号接收完成以前发生的。
  • 延迟保证
    • 一个大小=1的缓冲通道为您提供了一个延迟的保证。它能够保证发送的前一个信号已经收到。
      • 由于第一个信号的接收,在第二个信号发送完成以前就发生了。

缓冲区的大小毫不是一个随机数,它必须是为一些定义好的约束而计算出来的。在计算中没有无穷远,全部的东西都必须有一个明肯定义的约束,不管是时间仍是空间。

无数据信号

没有数据的信号,主要是为取消而预留的。它容许一个goroutine发出信号,让另外一个goroutine取消他们正在作的事情,而后继续前进。取消可使用非缓冲和缓冲channel来实现,可是在没有数据发送的状况下使用缓冲channel会更好。

内置函数 close 用于在没有数据的状况下发出信号。正如状态一节中介绍的,你仍然能够在一个关闭的通道接收到信号。事实上,在一个关闭的channel上的任何接收都不会阻塞,接收操做老是返回。

在大多数状况下,您但愿使用标准库 context 包来实现无数据的信号传递。context 包使用一个没有缓冲的channel来进行信号传递,而内置函数 close 发送没有数据的信号。

若是选择使用本身的通道进行取消,而不是 context 包,那么你的通道应该是 chan struct{} 类型的。这是一种零空间的惯用方式,用来表示一个仅用于信号传输的channel。

场景

有了这些属性,进一步了解它们在实践中工做的最佳方式就是运行一系列的代码场景。

有数据信号 - 保证 - 无缓冲的channel

当你须要知道发送的信号已经收到时,就会有两种状况出现。一种是等待任务,另外一种是等待结果。

场景 1 - 等待任务

设想你是一名经理,并雇佣了一名新员工。在这个场景中,你但愿你的新员工执行一个任务,可是他们须要等待,直到你准备好。这是由于你须要在他们开始以前给他们一份报告(paper)。

01 func waitForTask() {
02     ch := make(chan string)
03
04     go func() {
05         p := <-ch
06
07         // 员工执行工做
08
09         // 员工能够自由地去作
10     }()
11
12     time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
13
14     ch <- "paper"
15 }

在02行中,无缓冲的channel被建立,string 类型的数据将被发送到信号中。而后在04行,一名员工被雇佣,并被告知在05行前等待你的信号,而后再作他们的工做。第05行是channel接收,致使员工在等待你发送的文件时阻塞。一旦员工收到了这份报告,员工就完成了工做,而后就能够自由地离开了。

你做为经理正在和你的新员工一块儿工做。所以,当你在第04行雇佣了员工后,你会发现本身(在第12行)作了你须要作的事情来解阻塞而且通知员工。值得注意的是,不知道要花费多长的时间来准备这份报告(paper)。

最终你准备好给员工发信号了。在第14行,你执行一个带有数据的信号,数据就是那份报告(paper)。因为使用了一个没有缓冲的channel,因此当你的发送操做完成后,你就获得了该雇员已经收到该文件的保证。接收发生在发送以前。

场景2 - 等待结果

在接下来的场景中,事情发生了反转。这一次,你但愿你的新员工在被雇佣的时候当即执行一项任务,你须要等待他们工做的结果。你须要等待,由于在你能够继续以前,你须要他们的报告(paper)。

01 func waitForResult() {
02     ch := make(chan string)
03
04     go func() {
05         time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
06
07         ch <- "paper"
08
09         // 员工已经完成了,而且能够自由地离开
10     }()
11
12     p := <-ch
13 }

在第2行中,建立了一个没有缓冲的channel,该channel的属性是 string 型数据将被发送到信号。而后在第04行,一名雇员被雇佣,并当即被投入工做。当你在第04行雇佣了这名员工后,你会发现本身排在第12行,等待着这份报告。

一旦工做由第05行中的员工完成,他们就会在第07行经过有数据的channel发送结果给你。因为这是一个没有缓冲的通道,因此接收在发送以前就发生了,而且保证你已经收到告终果。一旦员工有了这样的保证,他们就能够自由地工做了。在这种状况下,你不知道他们要花多长时间才能完成这项任务。

成本/效益

一个没有缓冲的通道能够保证接收到的信号被接收。这很好,但没有什么是免费的。这种担保的成本是未知的延迟。在等待任务场景的过程当中,员工不知道要花多长时间才能发送那份报告。在等待结果的状况下,你不知道须要多长时间才能让员工发送结果。在这两种状况下,这种未知的延迟是咱们必需要面对的,由于须要保证。若是没有这种保证行为,逻辑是行不通的。

如下场景请你们结合以上内容,具体分析查看。

有数据信号 - 无保证 - 缓冲的 channel > 1

01 func fanOut() {
02     emps := 20
03     ch := make(chan string, emps)
04
05     for e := 0; e < emps; e++ {
06         go func() {
07             time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
08             ch <- "paper"
09         }()
10     }
11
12     for emps > 0 {
13         p := <-ch
14         fmt.Println(p)
15         emps--
16     }
17 }
01 func selectDrop() {
02     const cap = 5
03     ch := make(chan string, cap)
04
05     go func() {
06         for p := range ch {
07             fmt.Println("employee : received :", p)
08         }
09     }()
10
11     const work = 20
12     for w := 0; w < work; w++ {
13         select {
14             case ch <- "paper":
15                 fmt.Println("manager : send ack")
16             default:
17                 fmt.Println("manager : drop")
18         }
19     }
20
21     close(ch)
22 }

 有数据信号 - 延迟保证 - 缓冲channel 1 

01 func waitForTasks() {
02     ch := make(chan string, 1)
03
04     go func() {
05         for p := range ch {
06             fmt.Println("employee : working :", p)
07         }
08     }()
09
10     const work = 10
11     for w := 0; w < work; w++ {
12         ch <- "paper"
13     }
14
15     close(ch)
16 }

无数据信号 - 上下文(Context)

01 func withTimeout() {
02     duration := 50 * time.Millisecond
03
04     ctx, cancel := context.WithTimeout(context.Background(), duration)
05     defer cancel()
06
07     ch := make(chan string, 1)
08
09     go func() {
10         time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
11         ch <- "paper"
12     }()
13
14     select {
15     case p := <-ch:
16         fmt.Println("work complete", p)
17
18     case <-ctx.Done():
19         fmt.Println("moving on")
20     }
21 }

总结

在使用channel(或并发)时,关于保证、状态和发送的信号的属性很是重要。它们将帮助指导你实现你正在编写的并发程序和算法所需的最佳行为。它们将帮助你找到bug,并找出潜在的糟糕代码。

在这篇文章中,咱们分享了一些示例程序,它们展现了在不一样场景中信号的属性是如何工做的。每一个规则都有例外,可是这些模式是开始的良好基础。

相关文章
相关标签/搜索