-
channel
是goroutine
之间通信的一种方式,类似于UNIX中进程间通信方式中的管道。 -
channel
是Go语言中一种特殊的数据类型,类似UNIX系统中的管道或消息队列。
信道提供了一种机制在两个并发执行的函数之间进行同步,通过传递与该信道元素类型相符的值来进行通信。
如果说goroutine
是Go语言程序的并发体的话,channel
则是goroutine
之间的通信机制。每个channel
都是一个通信机制,可以让一个goroutine
通过它给另外一个goroutine
发送值数据。每个channel
都具有一个特殊的类型,也就是channel
可以发送数据的类型。
由于多个goroutine
为了争抢数据势必造成执行的低效率,channel则是一种类似队列的结构,通过使用队列以提高执行的效率。比如公共场所人多时,人们会采用排队的习惯以避免拥挤插队所导致的低效率资源使用和交换过程。因此,channel
类似一个传送带或队列,总是会遵循先进先出(FIFO, First In First Out)的规则,以保证收发数据的顺序。
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()
函数用于每次向通道中发送写入一个int
类型的数值,一个receive()
函数用于每次从通道中读取一个int
类型的数值。当通道中没有数据可读时,receive
的goroutine
会进入阻塞状态,因为receive
中使用了for
无限循环,也就时说receive
的goroutine
会一致阻塞下去,直到从通道中读取到数据。读取到数据后又会进入下一轮循环,由被阻塞在recv = <-ch
上。当main
函数中的休眠时间到了指定时间后,main
程序会终止也就意味着主程序结束,此时所有的goroutine
都会停止执行。
1
2
3
CSP
CSP经常被认为是Go在并发变成上成功的关键因素,CSP全称 Communicating Sequential Processes,由Tony Hoare于1978年发表在ACM上的一篇论文中提出,论文中指出一门编程语言应该重视input
和output
的原语,尤其是并发编程的代码。
传统的并发模型分为Actor模型和CSP模型,CSP模型由并发执行实体(进程、线程、协程)和消息通道构成,实体之间通过消息通道发送消息来进行通信。和Actor模型不同的是CSP模型关注的是消息发送的载体通道,而非发送消息的执行实体。
大多数编程语言的并发编程是基于线程和内存同步访问控制,Go的并发模型的模式则采用了goroutine
和channel
替代。goroutine
和线程类似,channel
和mutex
类似用于内存同步访问控制。
goroutine
和channel
是Go语言并发编程的两大基石,goroutine
用于执行并发任务,channel
用于goroutine
之间的同步和通信。channel
在goroutine
之间架起一条管道,在管道内传输数据,实现goroutine
之间的通信。由于channel
是线程安全的,因此使用起来非常方便。channel
还提供了先进先出的特性。
不要通过共享内存来通信,而是要通过通信来实现内存共享。
Channel
goroutine
运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine
奉行通过通信来共享内存,而非共享内存来通信。引用类型channel
是CSP模型的具体实现,用于多个goroutine
之间的通信,确保并发安全。
-
channel
是Go中的核心类型,可看作是一个管道,通过并发核心单元可发送或接收数据进行通讯。 -
channel
提供了一种通信机制,通过channel
一个goroutine
可以向另一个goroutine
发送消息。 -
channel
自身需关联一个类型,即channel
可以发送数据的类型。 -
channel
和map
类型类似,channel
拥有一个使用make()
创建的底层数据结构的引用。 -
channel
的零值也是nil
,可使用==
对类型相同的channel
比较,只有指向相同对象或同为nil
时才返回为true
。 -
channel
是线程安全的,多个goroutine
访问时无需加锁。 -
channel
本质上是一个数据结构即队列,数据按照先进先出FIFO:First In First Out
规则进行存取。
声明通道
- 通道本身需要一个类型进行修饰,类似切片类型需要表示元素类型。
- 通道的元素类型是在其内部传输的数据类型
var 通道变量 chan 通道类型
通道类型也就是通道内的数据类型,通道变量即保存通道的变量。chan
通道类型的空值是nil
,声明后需要配置make
才能使用。
- 通道是引用类型,通道必须初始化才能写入数据,即
make
后才能使用。 - 管道是有类型的
例如:声明通道并分配内存
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
向通道中写入数据时不能超过其容量
查看通道长度和容量
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
创建通道
- 通道是引用类型,因此需要使用
make
进行创建。 -
channel
是指针类型的数据类型,通过make
来分配内存。 -
channel
使用内置的make()
函数创建
通道实例 := make(chan 数据类型)
通道实例是通过make
创建的通道句柄,数据类型则表示通道内传输的数据类型。
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就是双向的,即可以接收数据,也可以发送数据。
chan T //可以接收和发送数据类型为T的数据
chan<- float64 //仅用于发送float64类型的数据
<-chan int //仅用于接收int类型的数据
<-
优先和最左侧类型结合
go
关键字用于开启goroutine
进行任务处理,多个任务之间若需通信则需使用通道。
例如:新开启的goroutine
的通道向通道发送一个值,然后在主线程的中接收这个值。
package main
import (
"fmt"
)
func main() {
intChan := make(chan int)
go func() {
intChan <- 1
}()
val := <-intChan
fmt.Println("value is ", val)
}
例如:通过通道传输自定义的结构体,此时一段修改数据并不会影响另一端的数据,通过通道传递后的数据是独立的。
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)
}
单向通道
单向通道真能惯用语写入用只能用于读取数据,通道本身是同时支持读写的。所谓的单向通道,只是对通道的一种使用限制。
声明只能写入数据的通道
var 通道实例 chan<- 元素类型
声明只能读取数据的通道
var 通道实例 <-chan 元素类型
例如:声明通道,设置通道只能单向写入。
ch := make(chan int)
var senderChannel chan<- int = ch
例如:声明只能单向写入的通道
ch := make(<-chan int)
通道容量
由于对通道的发送和接收操作都会在编译期间转换为底层的发送接收函数,因此通道分为两种带缓冲和不带缓冲的。对不带缓冲的通道进行操作时实际上可以看作是同步模式,带缓冲的则称之为异步模式。
同步模式下,发送方和接收方要同步就绪,只有在二者都ready
的情况下,数据才能在两者之间传输(实际上就是内存拷贝)。否则任意一方先行发送或接收操作都会被挂起,等待另一方的出现才能被唤醒。
异步模式下,在缓冲槽可用的情况下,也就是拥有剩余容量的情况下,发送和接收操作都可以顺序进行。否则操作方同样会被挂起,直到出现相反操作时才会被唤醒。
同步模式下必须要使发送方和接收方配对操作才会成功,否则会被阻塞。异步模式下,缓冲槽要有剩余容量操作才会成功,否则也会被阻塞。
使用make()
函数初始化通道是可以设置容量(capacity),容量表示通道容纳的最多的元素数量,即通道的缓存大小。若没有设置容量或容量设置为0则说明通道没有缓存,只有sender
和receiver
都准备完毕它们的通讯才会发生阻塞(blocking)。若设置了缓存即可能不会发生阻塞,只有缓存满了之后发送时才会阻塞,只有缓存空了后receiver
才会阻塞。一个nil
的通道是不会通信的。
声明通道时make(chan Type)
若没有指定容量则相当于make(chan Type, 0)
。
-
capacity = 0
表示通道是无缓冲阻塞读写的 -
capacity > 0
表示通道是有缓冲且非阻塞的,直到写满capacity
个元素后才会阻塞写入。
根据创建通道时是否设置容量可将通道分为两种类型,分别是unbuffered channel
和buffered channel
。
- 阻塞同步模式:无缓冲的通道
unbuffered 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
从通道中读取消息。通过无缓存的通道进行通信时,接收者接收到的数据会发生在发送者唤醒之前。
- 非阻塞异步模式:有缓冲的通道
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秒休眠结束,再次向通道中发送数据后,主线程读取到数据而不被阻塞。
通道状态
通道存在三种状态
-
nil
表示未初始化的状态,只进行了声明或手动赋值为nil
。 -
active
表示正常的通道,可读或可写。 -
closed
表示已关闭,注意关闭通道后channel
的值并非为nil
。
通道操作
通道是用来传递数据的一种数据结构,大部分时候通道会与goroutine
配合使用。通道可用于两个goroutine
之间通过传递一个指定类型的值来同步运行或通讯。操作符<-
用于指定通道的方向,用于发送或接收。若未指定方向则视为双向通道。
每个通道都具有三种操作分别是send
、receive
、close
-
send
表示sender
发送端的goroutine
向通道中投放数据 -
receive
表示receiver
接收端的goroutine
向通道中读取数据 -
close
表示关闭通道
通道操作符
通道采用<-
操作符来接收和发送数据
用法 | 描述 |
---|---|
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) //消费者 从信道中读取数据 打印输出
}