Go channel

作者: JunChow520 | 来源:发表于2021-02-18 18:52 被阅读0次

    单纯地将函数并发执行是没有意义的,函数与函数之间需要交换数据才能体现并发执行函数的作用。虽然可使用共享内存进行数据交换,但共享内存在不同的Goroutine中容易发生竟态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,但加锁会变成串行势必造成性能问题。

    Golang的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而非通过共享内存实现通信。

    Golang提倡使用通信的方式来代替共享内存,当一个资源需要在不同的Goroutine之间共享时,Channel会在Goroutine之间架起一个管道,以提供确保同步交换数据的机制。Goroutine是Golang程序并发的执行体,Channel是Goroutine之间的通信机制。每个Channel都是一个通信机制,可以让一个Goroutine通过它给另外一个Goroutine发送值数据。Channel提供了一种机制在两个并发执行的函数之间进行同步,通过传递与该Channel元素类型相符的值来进行通信。

    • Goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。
    • Channel是线程安全的,多个Goroutine访问时无需加锁。

    队列

    由于多个Goroutine为了争抢数据势必造成执行的低效率,通道使用了一种类似队列的结构,通过队列以提高执行的效率。Golang中Channel是一种特殊的数据类型,类似一个传送带或队列(本质上是一个数据结构-队列),数据遵循先进先出(FIFO, First In First Out)规则以保证收发数据顺序。

    goroutine和channel的通信

    通道声明

    channel是Golang一种特殊的数据类型,类似UNIX系统中的管道或消息队列,通过Goroutine可发送或接收数据进行通讯。声明channel时,需指定将要被共享数据的类型,channel自身必须关联一个数据类型(需要一个类型进行修饰),即channel可以发送数据的类型。

    var 变量 chan 类型
    

    channel类型也就是通道内元素传输的数据类型,通道类型的空值是nil

    var ch chan int
    fmt.Printf("value = %v, type = %T, address = %p\n", ch, ch, &ch)
    
    value = <nil>, type = chan int, address = 0xc000006028
    

    通道初始化

    channelmap类型类似,channel拥有一个使用make()创建的底层数据结构的引用(分配内存),因此 channel是引用类型,声明后需要使用make初始化后才能写入数据,同时向channel中写入数据时不能超过其容量。

    实例 := make(chan 元素类型, [缓冲大小])
    

    例如:声明int类型的channel,只能保存int类型的数据,也就是说一端只能向此channel中放入int类型的数据,另一端只能从此channel中读取int类型的值。

    ch := make(chan int, 100)
    

    channel实例是通过make创建的句柄,元素类型则表示Channel内传输的数据类型,当作为参数或返回值时需指定为类似ch chan int的格式。

    例如:声明channel并分配内存

    var ch chan int
    ch = make(chan int, 10)
    fmt.Printf("value = %v, type = %T, address = %p\n", ch, ch, &ch)
    
    value = 0xc00007e000, type = chan int, address = 0xc000006028
    

    例如:查看channel长度和容量

    var ch chan int
    ch = make(chan int, 10)
    fmt.Printf("value = %v, type = %T, address = %p, len = %d, cap = %d\n", ch, ch, &ch, len(ch), cap(ch))
    
    value = 0xc00007e000, type = chan int, address = 0xc000006028, len = 0, cap = 10
    

    通道缓存

    使用make()函数初始化channel时可以设置容量(capacity),容量表示通道容纳的最多的元素数量,即channel的缓存大小。

    ch := make(chan datatype, capacity)
    

    根据创建channel时是否设置容量可将其分为两种类型,分别是unbuffered channelbuffered channel

    • 若没有设置容量或容量为0说明channel没有缓存,发送方和接收方都准备完毕后通讯会发生阻塞(blocking)。
    • 若设置了缓存即可能不会发生阻塞,只有缓存满了之后发送时才会阻塞,只有缓存空了后接收方才会阻塞。而一个nil的通道是不会通信的。
    容量 缓冲 阻塞
    capacity = 0 无缓冲 阻塞读写
    capacity > 0 有缓冲 非阻塞,直到写满capacity个元素后才会阻塞写入。
    • 声明channelmake(chan Type)若没有指定容量则相当于make(chan Type, 0)
    ch1 := make(chan int)
    fmt.Printf("ch1: cap=%d\n", cap(ch1))//ch1: cap=0
    
    ch2 := make(chan int, 0)
    fmt.Printf("ch2: cap=%d\n", cap(ch2))//ch2: cap=0
    

    通道操作

    对通道的发送和接收操作都会在编译期间转换为底层的发送接收函数

    通道操作 示例 描述
    发送 ch<-10 将一个值发送到通道中
    接受 <-ch 从一个通道中接收值,若接受后未赋值则会忽略结果。
    关闭 close(ch) 关闭通道
    示例 描述
    channel <- value 发送value到channel
    <-channel 接收并将其丢弃
    val := <- channel 从通道中接收数据并赋值给val
    val, ok := <- channel 从通道中接收数据并赋值给val,同时检查通道是否已关闭或是否为空。

    关闭通道

    通道状态 描述
    nil 未初始化的状态,只进行了声明或手动赋值为nil
    active 正常通道,可读或可写。
    closed 已关闭,关闭后通道的值并非为nil

    关闭通道需调用Golang内置的close()函数来实现,需要注意的是,只有在通知接收方Goroutine所有数据都发送完毕时才需要关闭通道。通道可以被垃圾回收机制回收。和关闭文件不同的是,在结束操作之后关闭文件是必须的,但关闭通道却不是。

    close(ch)
    

    关闭通道操作原则上应该由发送方完成,若仍然向一个已关闭的通道发送数据则会导致程序抛出panic。若由接收者关闭通道也会触发panic风险。

    package main
    
    func main() {
        c := make(chan int, 2)
        close(c)
        c <- 1// panic: send on closed channel
    }
    

    从一个已经关闭的通道中读取数据时需要注意的是,接收者不会被一个已经关闭的通道阻塞。接收者从关闭的通道中仍然可以读取数据,不过此时是通道的数据据类型的默认值。此时可判断读取状态,若为false则表示通道已经被关闭。

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        c := make(chan int, 2)
        go func() {
            c <- 1
            time.Sleep(time.Second)
    
            c <- 2
            time.Sleep(time.Second)
    
            close(c)
        }()
        for i := 0; i < 4; i++ {
            val, ok := <-c
            fmt.Printf("receive %v status %t\n", val, ok)
        }
    }
    
    receive 1 status true
    receive 2 status true
    receive 0 status false
    receive 0 status false
    

    上例中工作goroutine关闭通道前,主goroutine仍然会被工作goroutine所阻塞,因此读取数据时,注意状态位。当工作goroutine关闭通道之后,主goroutine仍然可以从通道中读取int类型的默认值0,只不过此时状态变量会变为false,而且不再被阻塞,直到循环结束。

    通道方向

    ChannelType = ("chan" | "chan" "<-" | "<-" "chan") ElementType .
    
    方向 示例
    单向 (chan<- | <-chan) ElementType
    双向 chan ElementType

    channel类型包括三种类型的定义,可选的<-代表channel的方向,<-优先和最左侧类型结合。如果没有指定方向,那么channel就是双向的,即可以接收数据,也可以发送数据。

    chan T //可以接收和发送数据类型为T的数据
    chan<- float64 //仅用于发送float64类型的数据
    <-chan int //仅用于接收int类型的数据
    

    单向通道

    所谓的单向通道是对通道的一种读写使用限制。单向通道只能写入或读取数据,由于通道本身是同时支持读写的。

    操作 示例 描述
    只写 var 实例 chan<- 元素类型 声明只能写入数据的通道
    只读 var 实例 <-chan 元素类型 声明只能读取数据的通道

    例如:声明channel,设置只能单向写入。

    ch := make(chan int)
    var senderChannel chan<- int = ch
    

    例如:声明只能单向写入的channel

    ch := make(<-chan int)
    

    同步阻塞

    • 同步模式(阻塞):无缓冲的通道 unbuffered channel

    无缓冲的通道只有当发送方和接收方都准备好时才会传送数据,发送方和接收方要同步就绪,只有在二者都ready的情况下,数据才能在两者之间传输(实际上就是内存拷贝)。否则任意一方先行发送或接收操作都会被挂起,等待另一方的出现才能被唤醒。

    例如:无缓冲通道发送时引发死锁

    var ch chan int
    ch = make(chan int) //声明无缓冲的通道,读写阻塞。
    ch <- 1             //发生阻塞
    
    fatal error: all goroutines are asleep - deadlock!
    

    例如:无缓冲通道读取时引发死锁

    var ch chan int
    ch = make(chan int) //声明无缓冲的通道,读写阻塞。
    <-ch                //发生阻塞
    
    fatal error: all goroutines are asleep - deadlock!
    

    例如:发送方和接收方要同步就绪,只有在二者都ready的情况下,数据才能在两者之间传输(实际上就是内存拷贝)。

    var ch chan int
    ch = make(chan int) //声明无缓冲的通道,读写阻塞。
    ch <- 1             //发生阻塞
    go func() {
        <-ch
    }()
    
    fatal error: all goroutines are asleep - deadlock!
    

    例如:无缓冲通道只有当发送方和接收方都准备好时才会传送数据

    var ch chan int
    ch = make(chan int) //声明无缓冲的通道,读写阻塞。
    go func() {
        <-ch
    }()
    ch <- 1 //不会阻塞
    

    例如:为保证执行顺序添加sync.WaitGroup

    var wg sync.WaitGroup
    
    func main() {
        var ch chan int
        ch = make(chan int) //声明无缓冲的通道,读写阻塞。
    
        wg.Add(1)
        go func() {
            defer wg.Done()
            v := <-ch
            fmt.Printf("recieve:%d\n", v)
        }()
    
        v := 1
        ch <- v //不会阻塞
        fmt.Printf("send:%d\n", v)
        wg.Wait()
    }
    

    由于无缓冲这种阻塞发送方和接收方的特性,使用时需要防止死锁的发生。如果在一个Goroutine内向同一个Channel同时进行读取和发送则会导致死锁。

    例如:

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        c := make(chan int)
        go func() {
            fmt.Println("work:ready to send")
    
            c <- 1
            fmt.Println("work:send 1 to channel")
    
            fmt.Println("work:start sleep 1 second")
            time.Sleep(time.Second)
            fmt.Println("work:end sleep 1 second")
    
            c <- 2
            fmt.Println("work:send 2 to channel")
        }()
    
        fmt.Println("main:start sleep 1 second")
        time.Sleep(time.Second)
        fmt.Println("main:end sleep 1 second")
    
        val := <-c
        fmt.Println("main:receive value ", val)
    
        val = <-c
        fmt.Println("main:receive value ", val)
    
        time.Sleep(time.Second)
    }
    
    main:start sleep 1 second
    work:ready to send
    main:end sleep 1 second
    main:receive value  1
    work:send 1 to channel
    work:start sleep 1 second
    work:end sleep 1 second
    work:send 2 to channel
    main:receive value  2
    

    声明无缓冲通道后开启goroutine向通道发送数据,然后主线程从通道中读取数据。主线程休眠期间,goroutine阻塞在发送向通道发送数据的位置,只有当主线程休眠结束开始从通道中读取数据时,goroutine才开始向下运行。同时,当协程发送完第一个数据休眠时,主线程读取了第一个数据,准备从通道中读取第二个数据时会被阻塞,直到协程休眠结束向通道发送数据后才会继续运行。

    从无缓存的通道中读取消息时会阻塞,直到有goroutine向该通道发送消息。同理,向无缓存的通道中发送消息时也会阻塞,直到有goroutine从通道中读取消息。通过无缓存的通道进行通信时,接收者接收到的数据会发生在发送者唤醒之前。

    例如:

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func send(ch chan int) {
        ch <- 1
        ch <- 2
        ch <- 3
    }
    func receive(ch chan int) {
        var recv int
        for {
            recv = <-ch
            fmt.Println(recv)
        }
    }
    func main() {
        ch := make(chan int)
        go send(ch)
        go receive(ch)
        time.Sleep(time.Second * time.Duration(2))
    }
    

    主函数中开启两个goroutine,一个用于执行send()函数用于每次向channel中发送写入一个int类型的数值,一个receive()函数用于每次从通道中读取一个int类型的数值。当channel中没有数据可读时,receivegoroutine会进入阻塞状态,因为receive中使用了for无限循环,也就时说receivegoroutine会一致阻塞下去,直到从channel中读取到数据。读取到数据后又会进入下一轮循环,由被阻塞在recv = <-ch上。当main函数中的休眠时间到了指定时间后,main程序会终止也就意味着主程序结束,此时所有的goroutine都会停止执行。

    1
    2
    3
    

    异步非阻塞

    • 异步模式(非阻塞):有缓冲的通道 buffered channel

    异步模式下,在缓冲槽可用的情况下,也就是拥有剩余容量的情况下,发送和接收操作都可以顺序进行。否则操作方同样会被挂起,直到出现相反操作时才会被唤醒。

    异步模式下,缓冲槽要有剩余容量操作才会成功,否则也会被阻塞。

    通多缓存的使用可以尽量避免堵塞,以提供应用的性能。

    有缓存的通道类似一个阻塞队列(采用环形数组实现),当缓存未满时向通道中发送消息不会堵塞,当缓存满时,发送操作将被阻塞,直到有其它goroutine从中读取消息。相应的,当通道中消息不为空时,读取消息不会出现堵塞,当通道为空时,读取操作会造成阻塞,直到有goroutine向通道中写入消息。

    有缓存的通道区别在于只有当缓冲区被填满时才会阻塞发送者,只有当缓冲区为空时才会阻塞接收者。

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        c := make(chan int, 2)
        go func() {
            for i := 0; i < 4; i++ {
                c <- i
                fmt.Println("work:send ", i)
            }
            time.Sleep(time.Second * 5)
            for i := 4; i < 6; i++ {
                c <- i
                fmt.Println("work:send ", i)
            }
        }()
        for i := 0; i < 6; i++ {
            time.Sleep(time.Second)
            fmt.Println("main:receive ", <-c)
        }
    }
    
    work:send  0
    work:send  1
    main:receive  0
    work:send  2
    main:receive  1
    work:send  3
    main:receive  2
    main:receive  3
    work:send  4
    work:send  5
    main:receive  4
    main:receive  5
    

    声明容量为2带缓冲的通道,开启一个协程,这个协程会向通道连续发送4个数据后然后休眠5秒,然后再向通道发送2个数据。而主线程则会从这个通道中读取数据,每次读取前会先休眠1秒。goroutine首先向通道发送了两个数据分别为0和1后被阻塞,因为此时主线程在运行1秒的休眠。主线程休眠结束后,从通道中读取了第一个数据0后继续休眠1秒。通道此时又有了缓冲,于是goroutine又向通道发送了第三个数据2,而后再次因为通道的缓冲区已满则进入休眠。以此类推,直到协程将4个数据发送完毕后,才开始运行5秒的休眠。而当主线程从通道读取完第4个数据也就是3之后,当准备再从通道中读取第五个数据时,由于通道为空,主线程作为接收者被阻塞。直到goroutine的5秒休眠结束,再次向通道中发送数据后,主线程读取到数据而不被阻塞。

    应用

    超时处理

    channel配合select可实现多路复用,select写法类似switch不同之处在于select的每个case代表一个通信操作,即在某个信道上进行发送或接收的操作,同时会包含一些语句组成一个 语句块。

    select用于多个信道监听并收发消息,当任何一个条件满足时会执行,若没有可执行的case则会执行默认的case。若不存在默认的case则程序发生堵塞。

    select默认是堵塞的,只有监听的信道中有发送或接收的数据时才会运行。

    生产者消费者

    生产者消费者有一个著名的线程同步问题,即生产者产出后将产品交给若干消费者,为使生产者和消费者并发执行,两者之间会设置一个具有多个缓冲区的缓冲池,生产者将产出产品放入缓冲池,消费者从缓冲池取出产品,此时生产者和消费者之间必须保持同步,即不允许消费者到一个空缓冲区内获取产品,也不允许生产者向一个已经存放产品的缓冲区中再次投放产品。

    Go语言的channel信道天生具有这种特性,即当缓冲区满时写空时读都会被阻塞,另外channel本身就是并发安全的。

    使用单向信道创建生产者消费者模式

    package main
    
    import "fmt"
    
    func producer(out chan<- int) {
        for i := 0; i < 10; i++ {
            out <- i * i
        }
        close(out)
    }
    func consumer(in <-chan int) {
        for num := range in {
            fmt.Println("num = ", num)
        }
    }
    
    func main() {
        ch := make(chan int) //创建双向信道
        go producer(ch)      //创建并发执行单元作为生产者 生产数字写入信道
        consumer(ch)         //消费者 从信道中读取数据 打印输出
    }
    
    num =  0
    num =  1
    num =  4
    num =  9
    num =  16
    num =  25
    num =  36
    num =  49
    num =  64
    num =  81
    

    生产者消费者模式

    package main
    
    import (
        "fmt"
        "math/rand"
        "time"
    )
    
    func producer(out chan<- string) {
        for {
            out <- fmt.Sprintf("%v", rand.Float64())
            time.Sleep(time.Second * time.Duration(1))
        }
    }
    func consumer(in <-chan string) {
        for {
            msg, ok := <-in //若信道无数据则发生堵塞
            if !ok {
                fmt.Println("channel close")
                break
            }
            fmt.Println("msg = ", msg)
        }
    }
    
    func main() {
        ch := make(chan string, 5) //创建双向信道
        go producer(ch)            //创建并发执行单元作为生产者 生产数字写入信道
        consumer(ch)               //消费者 从信道中读取数据 打印输出
    }
    

    相关文章

      网友评论

        本文标题:Go channel

        本文链接:https://www.haomeiwen.com/subject/kcyszktx.html