zoukankan      html  css  js  c++  java
  • golang学习笔记---Channels特性

    Channels的可靠发送

    可靠保证的通信发送接受基于这么个场景“我是否需要保证已收到特定goroutine发送的信号?”,来看看下面的例子:

    go func() {
        p := <-ch // Receive
    }()
     
    ch <- "paper" // Send

    发送goroutine是否需要保证第二行的goroutine接收ch通道的字符串才可以继续?根据这个简单问题,我们可以定义出两种类型的通道:无缓冲和缓冲。在可靠发送上每种类型的Channels都体现了不同的行为特征,可以简单总结如下:

    Channels的状态

    通道的行为特性直接受到其当前状态的影响。其状态可以分为三种:nilopen或者closed。下面的代码注释中定义并说明了这三种状态的通道声明:

    // ** nil channel
     
    // A channel is in a nil state when it is declared to its zero value
    var ch chan string
     
    // A channel can be placed in a nil state by explicitly setting it to nil.
    ch = nil
     
     
    // ** open channel
     
    // A channel is in a open state when it’s made using the built-in function make.
    ch := make(chan string)    
     
     
    // ** closed channel
     
    // A channel is in a closed state when it’s closed using the built-in function close.
    close(ch)

    通道的状态确定了它是发送和接收操作的行为方式。需要注意,信号是通过通道来发送和接收的,但是不能叫作读/写,因为它不是IO操作。其对应的状态类型如下:

    当通道处于nil状态时,通道上尝试的任何发送或接收都将被阻止。 当通道处于打开状态时,可以发送和接收信号。 当通道处于关闭状态时,它将不再能够发送信号,但是仍然可以接收信号。

    这些状态将在你实际开发中遇到的不同情况提供所需的不同行为特征参考。 将状态与可靠传递结合时,就可以分析你的设计选择而形成的的付出成本与收获。在你了解通道的这些行为特性后,大多数情况下,你就能通过阅读代码快速地发现bug。

    Channels的是否传递数据

    需要考虑的最后一个信号属性是,需不需要在发送信号的时候携带数据。通过在通道上发送数据很简单,只需要如下编码就行:

    ch <- "paper"

    当你在使用了带数据的信号时,通常是因为:

    (1)要求goroutine执行新任务;

    (2)goroutine报告了结果。

    当然,你也可以通过关闭通道来发出无数据的信号,这种情况下一般是:

    (1)goroutine被告知需要停止其正在执行的任务;

    (2)goroutine执行完毕报告结果(无需数据);

    (3)goroutine报告已完成处理并关闭。

    没有数据的信号的一个好处是发送端goroutine可以立即向多个goroutine发出信号。 而带数据的信号一般是两个goroutines之间的1对1通信。

    带数据的Channels

    对于带数据的通道,根据你对数据的可靠性需要不同,这里有三类不同类型的配置参数,如下所示:

    三种参数分别为:UnbufferedBuffered >1 和 Buffered =1

    可靠保证:

    一个不带缓冲的channel将会在信号的从发送出去到接收到的过程中保持可靠的稳定性(因为信号的接收是在信号发送完成之前就已完成了的)。

    不可靠保证:

    一个带缓冲的channel(Buffered>1时)在信号在发送与接受之间将不提供可靠保证(这是因为在这种情况下,信号的接收是在信号发送完成之后才完成的)。

    延时保证:

    当Buffered为1时,这个时候将是一个延时保证的信号。这种情况下可以保证收到先前发送出去信号(这是因为第一个信号的接收是在第二个信号发送完成之前发生的)。

    所以,基于上面的三种不同情况,缓冲区的大小可不是随便定的,必须针对不同明确定义的条件进行不同的取值。

    不带数据的Channels

    没有数据的信号主要用于执行一些取消的动作。 它允许一个goroutine发出信号通知另一个goroutine取消他们正在做的事情并继续执行之后的业务。 当然,取消可以使用无缓冲或者缓冲通道来实现,但是当不带数据却使用带缓冲的通道时,这种代码就不太优雅了。

    内置的close函数用于在没有数据的情况下发出信号。 在上面的“状态”章节有说过,在已关闭的通道上依然是可以接收信号的。 实际上,已关闭的通道上的任何接收都是不会阻塞的,并且接收操作总会返回。

    在大多数情况下,我们一般使用标准库的context包来实现没有数据的信号。实际上context包的底层也是使用不带缓冲的通道用进行信号通信的,内置的close函数传递信号也是不带数据的。

    当你使用自己定义的channel进行取消操作而不是标准库中的context包时,channel应该定义为chan struct {}类型的。 这是表示仅用于信号通信的零占用空间的channel的惯用方式。

    实际应用举例

    带数据 – 可靠传递 – 不带缓冲的Channel

    当您需要知道已收到正在发送的信号时,会出现两种情况:等待任务(Wait For Task)和等待结果(Wait For Result)。

    场景1:等待任务

    这种情况就如同你作为一名经理雇用了一名新员工。你希望新员工马上开始执行任务,而他却需要等待到你准备好

    package main
    
    import (
        "fmt"
        "math/rand"
        "time"
    )
    
    func main() {
    
        ch := make(chan string)
    
        go func() {
            p := <-ch
            fmt.Println(p)
    
            // Employee performs work here.
            // Employee is done and free to go.
        }()
        ch <- "paper"
        time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
    
    }

    上面的代码中,创建了一个Unbuffered的通道,主进程中发送数据到通道,在go关键字创建的goroutine就会接收到,并继续执行它之后的业务逻辑。

    场景2:等待结果

    这种情况正好相反。 这次你希望新的goroutine在创建时立即执行,并在主goroutine中等待结果,下面的代码实例可以简单说明:

    package main
    
    
    import (
    	"fmt"
    	"math/rand"
    	"time"
    )
    
    
    func main() {
    
    
    	ch := make(chan string)
    
    
    	go func() {
    		time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
    		ch <- "paper"
    		// Employee is done and free to go.
    	}()
    
    
    	p := <-ch
    	fmt.Println(p)
    
    
    } 

    优缺点对比:

    无缓冲通道可确保接收到正在发送的信号。 但是这种保证的成本是损失是带来了未知的延迟。 在等待任务方案中,新goroutine不知道发送该ch消息需要等待多长时间,而在等待结果方案中,主goroutine不知道新创建的那个goroutine将多久后将结果发送回来。

    带数据 – 不可靠传递 – 带缓冲(Buffer>1)的Channel

    当您不需要知道已收到正在发送的信号时,这两种情况就会发挥作用:Fan Out模式和Drop模式。

    带缓冲的通道明确地定义的空间大小用于存储正在发送的数据。 那么我们应该如何确定到底需要多少空间呢?可以从下面几个方面考虑:(1)工作量制定是否完善?(3)如果员工无法跟上,是否会影响你当下的工作?(3)如果程序意外终止,有多大可接受的风险?

    Fan Out模式

    fan out模式允许你针对同一问题创建多个的worker进行处理。 由于每个任务都是由一个goroutine进行处理,所以你可以确切的知道将收到多少结果。但是如果他们同时发送结果时,就会存在竞争现象,造成他们阻塞循环等待结果。

    还是上面的场景,但这次你创建了多个goroutine来单独处理任务。 这个场景的工作情况可如下描述:

    package main
    
    import (
        "fmt"
        "math/rand"
        "time"
    )
    
    func main() {
    
        emps := 20
        ch := make(chan string, emps)
    
        for e := 0; e < emps; e++ {
            go func() {
                time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
                ch <- "paper"
            }()
        }
    
        for emps > 0 {
            p := <-ch
            fmt.Println(p)
            emps--
        }
    
    }

    在上述代码的中,创建了一个可指定大小的字符串类型的缓冲通道,创建了20个goroutine并立即开始工作。但是主goroutine并不知道每个新的goroutine需要处理多长时间。而在最后的for循环中,主goroutine循环等待所有新建的goroutine都处理完成,才确定全部任务执行完毕。

    Drop模式

    在Drop模式下,当所有的工作goroutine都在满负荷时,将放弃新的任务。 这样做的好处是可以继续接收要处理的任务,而不会造成工作goroutine压力过后或者延迟。 所以,这里的关键在于如何知道工作goroutine可以接受新的任务,什么时候知道工作goroutine压力过大。

    package main
    
    import (
        "fmt"
    )
    
    func main() {
    
        const cap = 5
        ch := make(chan string, cap)
    
        go func() {
            for p := range ch {
                fmt.Println("employee : received :", p)
            }
        }()
    
        const work = 20
        for w := 0; w < work; w++ {
            select {
            case ch <- "paper":
                fmt.Println("manager : send ack")
            default:
                fmt.Println("manager : drop")
            }
        }
    
        close(ch)
    
    }

    不带数据的信号- Context

    在最后这个场景中,将讲到如何使用Go的标准库中的context包中的Context来取消正在运行的goroutine。当然这也可以使用关闭的无缓冲的通道来执行没有数据的信号来实现。实例代码如下:

    package main
    
    import (
        "context"
        "fmt"
        "math/rand"
        "time"
    )
    
    func main() {
    
        duration := 50 * time.Millisecond
    
        ctx, cancel := context.WithTimeout(context.Background(), duration)
        defer cancel()
    
        ch := make(chan string, 1)
    
        go func() {
            time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
            ch <- "paper"
        }()
    
        select {
        case p := <-ch:
            fmt.Println("work complete", p)
    
        case <-ctx.Done():
            fmt.Println("moving on")
        }
    
    }

    实际上,context包会创建一个goroutine,一旦超时时间到了,它将关闭与Context值关联的无缓存的通道。 当然你也可以通过调用WithTimeout函数返回的cancel来主动取消,以清除掉Context创建的内容。 另外cancel函数也可以被多次调用。

    在实际项目中,使用带缓冲(Buffer=1)的通道是比较合适的,如果使用无缓冲的通道的时候,当工作goroutine长期处于阻塞状态也无法被唤醒时就会造成goroutine leak。goroutine leak同内存泄漏一样可怕,这样的 goroutine 会不断地吞噬资源,导致系统运行变慢,甚至是崩溃。当然这个并不在本文的讨论范围之中。

    小 结

    可靠保证、通道状态和是否带缓冲对于理解Channel或者Go并发是非常重要的。尤其是在你编写高并发或者算法性程序的时候。这篇通过示例程序,展示了通道属性在不同的场景中如何工作。可以简单总结如下:

    语言特点:

    (1)使用通道来协调和协调goroutines

    专注于通信属性而不是数据共享;

    带数据的通信和不带数据的通信;

    明确同步对于共享状态的影响;

    (2)无缓冲的通道:

    接收发生在发送之前;

    好处:100%保证已收到信号;

    不足:接收信号存在未知延迟;

    (3)有缓冲的通道:

    发送发生在接收之前;

    好处:减少信号之间的阻塞延迟;

    不足:无法保证何时收到信号(buffer越大,可以延迟越大);

    (4)关闭通道:

    关闭发生在接收之前(同缓冲通道一样);

    不带数据通信;

    适用于信号取消和超时时间;

    (5)nil类型的通道:

    发送和接收都是阻塞式;

    关闭通信;

    适用于限速或短期停止;

     

    设计哲学:

    (1)如果通道上发送的信号会导致发送goroutine阻止:

    不允许使用(Buffer>1)的缓冲通道;

    必须知道当goroutine发送信号阻塞时会发生什么;

    (2)如果通道上发送的信号不会导致发送goroutine阻止:

    每次发送都有明确的缓冲区大小(Fan Out模式);

    有缓冲区最大容量限制(Drop模式);

    (3)缓冲区越小越好:

    考虑缓冲时,不要考虑性能;

    缓冲可以减少信号之间的阻塞延迟;

    如果带缓冲的通道能提供更大的并发量那更好(更大并发量更小缓冲区原则)。

    原文参考:

    https://www.ardanlabs.com/blog/2017/10/the-behavior-of-channels.html

  • 相关阅读:
    系统综合实践第三次实践
    系统综合实践第二次实践作业
    第1次实践作业
    软工总结
    团队Beta演示
    团队Beta5
    软工实践个人总结
    第09组 Beta版本演示
    第09组 Beta冲刺(5/5)
    第09组 Beta冲刺(4/5)
  • 原文地址:https://www.cnblogs.com/saryli/p/13365922.html
Copyright © 2011-2022 走看看