Go channel

channel提供了一种机制在两个并发执行的函数之间进行同步,通过传递与该channel元素类型相符的值来进行通信。

goroutine是Go语言程序的并发体,channelgoroutine之间的通信机制。每个channel都是一个通信机制,可以让一个goroutine通过它给另外一个goroutine发送值数据。每个channel都具有一个特殊的类型,也就是channel可以发送数据的类型。

队列

  • channel是Go中的核心类型,可看作是一个管道,通过并发核心单元可发送或接收数据进行通讯。
  • channel提供了一种通信机制,通过channel一个goroutine可以向另一个goroutine发送消息。
  • channel自身需关联一个类型,即channel可以发送数据的类型。
  • channelmap类型类似,channel拥有一个使用make()创建的底层数据结构的引用。
  • channel的零值也是nil,可使用==对类型相同的channel比较,只有指向相同对象或同为nil时才返回为true
  • channel是线程安全的,多个goroutine访问时无需加锁。
  • channel本质上是一个数据结构即队列,数据按照先进先出FIFO:First In First Out规则进行存取。
  • channel是Golang一种特殊的数据类型,类似UNIX系统中的管道或消息队列。

由于多个goroutine为了争抢数据势必造成执行的低效率,channel则是一种类似队列的结构,通过使用队列以提高执行的效率。比如公共场所人多时,人们会采用排队的习惯以避免拥挤插队所导致的低效率资源使用和交换过程。

  • channel类似一个传送带或队列,总是会遵循先进先出(FIFO, First In First Out)的规则,以保证收发数据的顺序。
  • channelgoroutine之间通信的一种方式,类似于UNIX中进程间通信方式中的管道。
goroutine和channel的通信

Go语言提倡使用通信的方式来代替共享内存,当一个资源需要在不同的goroutine之间共享时,channel会在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

goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine奉行通过通信来共享内存,而非共享内存来通信。引用类型channel是CSP模型的具体实现,用于多个goroutine之间的通信,确保并发安全。

声明

  • channel本身需要一个类型进行修饰,类似切片类型需要表示元素类型。
  • channel的元素类型是在其内部传输的数据类型
var 变量 chan 类型

channel类型也就是通道内的数据类型,channel变量即保存通道的变量。chan通道类型的空值是nil,声明后需要配置make才能使用。

  • channel是引用类型,必须初始化才能写入数据,即make后才能使用。

例如:声明channel并分配内存

var ch chan int
ch = make(chan int, 10)
fmt.Printf("value is %v, address is %p\n", ch, &ch)//value is 0xc0000de000, address is 0xc0000d8018
  • channel中写入数据时不能超过其容量

例如:查看channel长度和容量

var ch chan int
ch = make(chan int, 10)
fmt.Printf("len is %v, capacity is %v\n", len(ch), cap(ch))//len is 0, capacity is 10

创建

  • channel是引用类型(指针),需使用内置make()函数分配内存后才能创建。
实例 := make(chan 数据类型)

channel实例是通过make创建的句柄,数据类型则表示channel内传输的数据类型。

ch := make(chan Type, capacity)

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

ch := make(chan int, 100)

类型

  • chan TYPE表示channel的类型,当作为参数或返回值时需指定为xxx chan int类似的格式。
ChannelType = ("chan" | "chan" "<-" | "<-" "chan") ElementType .

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

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

go关键字用于开启goroutine进行任务处理,多个任务之间若需通信则需使用channel

例如:新开启的goroutinechannel发送一个值,然后在主线程的中接收这个值。

package main

import (
    "fmt"
)

func main() {
    intChan := make(chan int)
    go func() {
        intChan <- 1
    }()
    val := <-intChan
    fmt.Println("value is ", val)
}

例如:通过channel传输自定义的结构体,一端的修改并不会影响另一端的数据,通过channel传递后的数据是独立的。

package main

import (
    "fmt"
)

type Addr struct {
    City     string
    District string
}

type User struct {
    Id      int
    Name    string
    Address Addr
}

func main() {
    addr := Addr{"changsha", "meixihu"}
    user := User{1, "admin", addr}

    userChannel := make(chan User, 1)
    userChannel <- user

    obj := <-userChannel
    fmt.Printf("%+v", obj)
}

单向

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

声明只能写入数据的channel

var 实例 chan<- 元素类型

声明只能读取数据的channel

var 实例 <-chan 元素类型

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

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

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

ch := make(<-chan int)

容量

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

ch := make(chan datetype, capacity)

若没有设置容量或容量设置为0则说明channel没有缓存,只有senderreceiver都准备完毕它们的通讯才会发生阻塞(blocking)。若设置了缓存即可能不会发生阻塞,只有缓存满了之后发送时才会阻塞,只有缓存空了后receiver才会阻塞。一个nil的通道是不会通信的。

声明channelmake(chan Type)若没有指定容量则相当于make(chan Type, 0)

  • capacity = 0表示channel是无缓冲,阻塞读写的。
  • capacity > 0表示channel是有缓冲且非阻塞的,直到写满capacity个元素后才会阻塞写入。

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

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

无缓冲

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

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

同步模式下,必须要使发送方和接收方配对操作才会成功,否则会被阻塞。

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

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从通道中读取消息。通过无缓存的通道进行通信时,接收者接收到的数据会发生在发送者唤醒之前。

有缓冲

  • 异步模式(非阻塞):有缓冲的通道 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存在三种状态

  • nil表示未初始化的状态,只进行了声明或手动赋值为nil
  • active表示正常的channel,可读或可写。
  • closed表示已关闭,注意关闭后channel的值并非为nil

操作

channel是用来传递数据的一种数据结构,大部分时候channel会与goroutine配合使用。

channel可用于两个goroutine之间通过传递一个指定类型的值来同步运行或通讯。操作符<-用于指定channel的方向,用于发送或接收。若未指定方向则视为双向通道。

每个channel都具有三种操作分别是sendreceiveclose

  • send表示sender发送端的goroutinechannel中投放数据
  • receive表示receiver接收端的goroutinechannel中读取数据
  • close表示关闭channel

方向操作符

channel采用<-操作符来接收和发送数据

用法 描述
channel <- value 发送value到channel
<-channel 接收并将其丢弃
val := <- channel 从通道中接收数据并赋值给val
val, ok := <- channel 从通道中接收数据并赋值给val,同时检查通道是否已关闭或是否为空。

关闭

  • 使用close关键字管理通道
  • 关闭通道的操作原则上应该由发送方完成,因为如果仍然向一个已关闭的通道发送数据会导致程序抛出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,而且不再被阻塞,直到循环结束。

超时机制

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)               //消费者 从信道中读取数据 打印输出
}

推荐阅读更多精彩内容

  • channel一个类型管道,通过它可以在goroutine之间发送和接收消息。它是Golang在语言层面提供的go...
    蔡欣圻阅读 4,979评论 3 10
  • 介绍 channel 提供了一种通信机制,通过它,一个goroutine可以向另外一个goroutine发送消息。...
    myvic_091阅读 37评论 0 0
  •    在golang中,channel属于较为核心的一个功能,尤其在go协程中,channel功能尤为重要。作为g...
    北春南秋阅读 4,686评论 2 2
  • 0. 引言 channel 是 Go 语言中的一个非常重要的特性,这篇文章来深入了解一下 channel。 [ht...
    陈Sir的知识回廊阅读 74评论 0 0
  • goroutine特性: runtime.Gosched():出让当前cpu时间片,当再次获得cpu时,从...
    骑蜗上高速阅读 53评论 0 0