GoroutinesAndChannels
⚠️Go语言中的并发是通过goroutines和channels来实现的。Goroutines是一种轻量级的执行线程,而Channels是用来在goroutines之间传递数据的管道。
Goroutines
PS:
1、线程(Thread) :有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程的切换一般也由操作系统调度。
2、协程(coroutine) :又称微线程与子例程(或者称为函数)一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。 和线程类似,共享堆,不共享栈,协程的切换一般由程序员在代码中显式控制。它避免了上下文切换的额外耗费,兼顾了多线程的优点,简化了高并发程序的复杂。
3、Goroutine和其他语言的协程(coroutine)在使用方式上类似,就是协程是一种协作任务控制机制,在最简单的意义上,协程不是并发的,而Goroutine支持并发的。因此Goroutine可以理解为一种Go语言的协程。同时它可以运行在一个或多个线程上。
1、Goroutine是Go中最基本的执行单元,是Go语言中的一个非常重要的特性,它是一种轻量级的线程,由Go语言的运行时(runtime)调度
2、每一个Go程序至少有一个goroutine:主goroutine。当程序启动时,它会自动创建。
3、goroutine采用了一种fork-join的模型。
4、与传统的线程相比,Goroutine的成本非常小,每个Goroutine只需要2KB的栈内存(在需要的时候会自动扩展),而线程则需要1MB的栈内存。
//只需要在函数调用前加上go关键字即可
sayHello := fun() {
fmt.println("Hello")
}
go sayHello()
进行join goroutine,需要引入wait操作:
var wg sync.WaitGroup()
sayHello := fun() {
defer wg.Done()
fmt.Println("Hello")
}
wg.Add(1)
go sayHello()
wa.wait()
优点:
1、开销小
POSIX的thread API虽然能够提供丰富的API,例如配置自己的CPU亲和性,申请资源等等,线程在得到了很多与进程相同的控制权的同时,开销也非常的大,在Goroutine中则不需这些额外的开销,所以一个Golang的程序中可以支持10w级别的Goroutine。
每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少(*goroutine:*2KB ,线程:8MB)
2、调度性能好
在Golang的程序中,操作系统级别的线程调度,通常不会做出合适的调度决策。例如在GC时,内存必须要达到一个一致的状态。在Goroutine机制里,Golang可以控制Goroutine的调度,从而在一个合适的时间进行GC。
在应用层模拟的线程,它避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度。
缺点:
协程调度机制无法实现公平调度。
Channel
- Channel基本操作
1、读写channel
goroutine是Go语言的基本调度单位,而channels则是它们之间的通信机制。操作符<-用来指定管道的方向,发送或接收。如果未指定方向,则为双向管道。
// 创建一个双向channel
ch := make(chan interface{})
interface{}表示chan可以为任何类型
channel有发送和接受两个主要操作。发送和接收两个操作都使用<-运算符。在发送语句中,channel放<-运算符左边。在接收语句中,channel放<-运算符右边。一个不使用接收结果的接收操作也是合法的。
// 发送操作
ch <- x
// 接收操作
x = <-ch
// 忽略接收到的值,合法
<-ch
我们不能弄错channel的方向:
writeStream := make(chan<- interface{})
readStream := make(<-chan interface{})
<-writeStream
readStream <- struct{}{}
上面的语句会产生如下错误:
invalid operation: <-writeStream (receive from send-only type chan<- interface {}) invalid operation: readS
2、关闭channel
Channel支持close操作,用于关闭channel,后面对该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。
从已经关闭的channel中读:
intStream := make(chan int)
close(intStream)
integer, ok := <- intStream
fmt.Pritf("(%v): %v", ok, integer)
// (false): 0
上面例子中通过返回值ok来判断channel是否关闭,我们还可以通过range这种更优雅的方式来处理已经关闭的channel:
intStream := make(chan int)
go func() {
defer close(intStream)
for i:=1; i<=5; i++{
intStream <- i
}
}()
for integer := range intStream {
fmt.Printf("%v ", integer)
}
// 1 2 3 4 5
### 2、无缓冲(unbuffered)的channel
无缓冲的channel是最常用的一种channel类型。
特点:在发送操作和接收操作之前,必须有一个Goroutine在等待。如果没有,那么发送操作和接收操作都将发生阻塞。
下面的例子展示了如何使用无缓冲的channel进行同步操作。在这个例子中,我们创建了一个无缓冲的channel,goroutine1向其中发送一个数据后,就一直等待goroutine2将数据接收走,然后才能继续执行。这样保证了goroutine1和goroutine2之间的同步。
ch := make(chan int)
go func() {
ch <- 10 // 发送数据
}()
x := <-ch // 接收数据
如果我们在没有接收方的情况下向channel发送数据,程序将会阻塞:
ch := make(chan int)
ch <- 10 // 阻塞
如果我们在没有发送方的情况下从channel接收数据,程序也将会阻塞:
ch := make(chan int)
x := <-ch // 阻塞
除了使用阻塞式的发送和接收操作之外,我们还可以使用select语句来实现非阻塞的发送和接收操作。下面的例子展示了如何使用select语句来从两个无缓冲的channel中接收数据:
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
ch1 <- 10
}()
go func() {
ch2 <- 20
}()
select {
case x := <-ch1:
fmt.Println(x)
case y := <-ch2:
fmt.Println(y)
}
3、带缓冲(buffered)的channel
创建了一个可以持有三个字符串元素的带缓冲Channel:
ch = make(chan string, 3)
我们可以在无阻塞的情况下连续向新创建的channel发送三个值:
ch <- "A"
ch <- "B"
ch <- "C"
此刻,channel的内部缓冲队列将是满的,如果有第四个发送操作将发生阻塞。
如果我们接收一个值:
fmt.Println(<-ch) // "A"
那么channel的缓冲队列将不是满的也不是空的,因此对该channel执行的发送或接收操作都不会发生阻塞。通过这种方式,channel的缓冲队列缓冲解耦了接收和发送的goroutine。
带缓冲的信道可被用作信号量,例如限制吞吐量。在此例中,进入的请求会被传递给 handle,它从信道中接收值,处理请求后将值发回该信道中,以便让该 “信号量” 准备迎接下一次请求。信道缓冲区的容量决定了同时调用 process 的数量上限。
var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
sem <- 1 // 等待活动队列清空。
process(r) // 可能需要很长时间。
<-sem // 完成;使下一个请求可以运行。
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // 无需等待 handle 结束。
}
}
然而,它却有个设计问题:尽管只有 MaxOutstanding 个 goroutine 能同时运行,但 Serve 还是为每个进入的请求都创建了新的 goroutine。其结果就是,若请求来得很快, 该程序就会无限地消耗资源。为了弥补这种不足,我们可以通过修改 Serve 来限制创建 Go 程,这是个明显的解决方案,但要当心我们修复后出现的 Bug。
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req) // 这儿有 Bug,解释见下。
<-sem
}()
}
}
Bug 出现在 Go 的 for 循环中,该循环变量在每次迭代时会被重用,因此 req 变量会在所有的 goroutine 间共享,这不是我们想要的。我们需要确保 req 对于每个 goroutine 来说都是唯一的。有一种方法能够做到,就是将 req 的值作为实参传入到该 goroutine 的闭包中:
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func(req *Request) {
process(req)
<-sem
}(req)
}
}
另一种解决方案就是以相同的名字创建新的变量,如例中所示:
func Serve(queue chan *Request) {
for req := range queue {
req := req // 为该 Go 程创建 req 的新实例。
sem <- 1
go func() {
process(req)
<-sem
}()
}
}
下面再看一个Go语言圣经的例子。它并发地向三个镜像站点发出请求,三个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓冲channel,最后接收者只接收第一个收到的响应,也就是最快的那个响应。因此mirroredQuery函数可能在另外两个响应慢的镜像站点响应之前就返回了结果。
func mirroredQuery() string {
responses := make(chan string, 3)
go func() { responses <- request("asia.gopl.io") }()
go func() { responses <- request("europe.gopl.io") }()
go func() { responses <- request("americas.gopl.io") }()
// 仅仅返回最快的那个response
return <-responses
}
func request(hostname string) (response string) { /* ... */ }
如果我们使用了无缓冲的channel,那么两个慢的goroutines将会因为没有人接收而被永远卡住。这种情况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。
4、Channels of channels
Go 最重要的特性就是信道是first-class value,它可以被分配并像其它值到处传递。 这种特性通常被用来实现安全、并行的多路分解。
我们可以利用这个特性来实现一个简单的RPC。 以下为 Request 类型的大概定义。
type Request struct {
args []int
f func([]int) int
resultChan chan int
}
客户端提供了一个函数及其实参,此外在请求对象中还有个接收应答的信道。
func sum(a []int) (s int) {
for _, v := range a {
s += v
}
return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// 发送请求
clientRequests <- request
// 等待回应
fmt.Printf("answer: %d\n", <-request.resultChan)
服务端的handler函数:
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
5、Channels pipeline (串联
Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。下面的程序用两个channels将三个goroutine串联起来:
第一个goroutine是一个计数器,用于生成0、1、2、……形式的整数序列,然后通过channel将该整数序列发送给第二个goroutine;第二个goroutine是一个求平方的程序,对收到的每个整数求平方,然后将平方后的结果通过第二个channel发送给第三个goroutine;第三个goroutine是一个打印程序,打印收到的每个整数。
func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}
func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}
6、Select多路复用
select用于从一组可能的通讯中选择一个进一步处理。如果任意一个通讯都可以进一步处理,则从中随机选择一个,执行对应的语句。否则,如果又没有默认分支(default case),select语句则会阻塞,直到其中一个通讯完成。
select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default:
// ...
}
如何使用select语句为一个操作设置一个时间限制。代码会输出变量news的值或者超时消息,具体依赖于两个接收语句哪个先执行:
select {
case news := <-NewsAgency:
fmt.Println(news)
case <-time.After(time.Minute):
fmt.Println("Time out: no news in one minute.")
}
下面的select语句会在abort channel中有值时,从其中接收值;无值时什么都不做。这是一个非阻塞的接收操作;反复地做这样的操作叫做“轮询channel”。
select {
case <-abort:
fmt.Printf("Launch aborted!\n")
return
default:
// do nothing
}