参考《gopl》
pdf: https://books.studygolang.com/download/gopl-zh.pdf
官网:The Go Programming Language
github code:GitHub - adonovan/gopl.io: Example programs from "The Go Programming Language"
仅做个人笔记,浏览请看原书
网络编程中使用goroutines
轮训客户端请求:
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err) // e.g., connection aborted
continue
}
go handleConn(conn) // handle connections concurrently
}
channel的关闭
Channel 还支持 close 操作,close(ch),来关闭 channel,随后对基于该 channel的任何发送操作都将导致 panic 异常。对一个已经被close 过的channel 仍然可以作接受操作;如果在关闭之前,channel中有数据的话就正常读取,如果channel中没有数据的话,将产生一个0值的数据 。
试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。
channel缓冲区
ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3
无缓冲区的channel也被称为同步channel
channel有无缓冲区的区别
无缓冲区的channel是同步的,否则是非同步的
c1:=make(chan int) 无缓冲
c2:=make(chan int,1) 有缓冲
c1<-1:无缓冲的,向c1通道放1时,会阻塞着,一直等到有别的协程作<-c1时,c1<-1才会继续后面的
c2<-1:则不会阻塞,因为缓冲大小是1 只有当 放第二个值的时候 第一个还没被人拿走,这时候才会阻塞。
· 有缓冲区channel的例子:(在 gopl-9. 基于共享变量的并发 的 sync.Mutex互斥锁 节里)
var (
sema = make(chan struct{}, 1) // a binary semaphore guarding balance
balance int
)
func Deposit(amount int) {
sema <- struct{}{} // acquire token
balance = balance + amount
<-sema // release token
}
func Balance() int {
sema <- struct{}{} // acquire token
b := balance
<-sema // release token
return b
}
· 无缓冲区channel的例子看下面的 ‘channel同步’
channel 同步
//!+
func main() {
conn, err := net.Dial("tcp", "localhost:8000")
if err != nil {
log.Fatal(err)
}
done := make(chan struct{})
go func() {
io.Copy(os.Stdout, conn) // NOTE: ignoring errors
log.Println("done")
done <- struct{}{} // signal the main goroutine
}()
mustCopy(conn, os.Stdin)
conn.Close()
<-done // wait for background goroutine to finish
}
//!-
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
channel 串联(pipeline)
//!+
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; ; x++ {
naturals <- x
}
}()
// Squarer
go func() {
for {
x := <-naturals
squares <- x * x
}
}()
// Printer (in main goroutine)
for {
fmt.Println(<-squares)
}
}
//!-
当限制次数时,Squarer可以不用等待,所以要监测一个关闭事件:
// Squarer
go func() {
for {
x, ok := <-naturals
if !ok {
break // channel was closed and drained
}
squares <- x * x
}
close(squares)
}()
没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结
果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示
channels已经被关闭并且里面没有值可接收。
使用range遍历channel,优雅的解决上述问题:
//!+
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()
// Squarer
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()
// Printer (in main goroutine)
for x := range squares {
fmt.Println(x)
}
}
//!-
单方向的channel
//!+
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)
}
//!-
带缓存的channel
向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部 删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收 操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个 goroutine执行发送操作而向队列插入元素。
并发地向三个镜像站点发出请求的例子
并发地向三个镜像站点发出请求,三 个镜像站点分散在不同的地理位置。它们分别将收到的响应发送到带缓存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") }()
return <-responses // return the quickest response
}
func request(hostname string) (response string) { /* ... */ }
如果我们使用了无缓存的channel,那么两个慢的goroutines将会因为没有人接收而被永远卡 住。这种情况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不同,泄漏的goroutines 并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的。
蛋糕店的例子
Channel的缓存也可能影响程序的性能。想象一家蛋糕店有三个厨师,一个烘焙,一个上糖 衣,还有一个将每个蛋糕传递到它下一个厨师在生产线。在狭小的厨房空间环境,每个厨师 在完成蛋糕后必须等待下一个厨师已经准备好接受它;这类似于在一个无缓存的channel上进 行沟通。
如果在每个厨师之间有一个放置一个蛋糕的额外空间,那么每个厨师就可以将一个完成的蛋 糕临时放在那里而马上进入下一个蛋糕在制作中;这类似于将channel的缓存队列的容量设置 为1。只要每个厨师的平均工作效率相近,那么其中大部分的传输工作将是迅速的,个体之间 细小的效率差异将在交接过程中弥补。如果厨师之间有更大的额外空间——也是就更大容量 的缓存队列——将可以在不停止生产线的前提下消除更大的效率波动,例如一个厨师可以短 暂地休息,然后再加快赶上进度而不影响其他人。
另一方面,如果生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都 将是满的。相反,如果后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是 空的。对于这类场景,额外的缓存并没有带来任何好处。
生产线的隐喻对于理解channels和goroutines的工作机制是很有帮助的。例如,如果第二阶段 是需要精心制作的复杂操作,一个厨师可能无法跟上第一个厨师的进度,或者是无法满足第 三阶段厨师的需求。要解决这个问题,我们可以雇佣另一个厨师来帮助完成第二阶段的工 作,他执行相同的任务但是独立工作。这类似于基于相同的channels创建另一个独立的 goroutine。
代码来自:gopl.io/ch8/cake at master · adonovan/gopl.io · GitHub 仅做个人备份,浏览请看原文
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// See page 234.
// Package cake provides a simulation of
// a concurrent cake shop with numerous parameters.
//
// Use this command to run the benchmarks:
// $ go test -bench=. gopl.io/ch8/cake
package cake
import (
"fmt"
"math/rand"
"time"
)
type Shop struct {
Verbose bool
Cakes int // number of cakes to bake
BakeTime time.Duration // time to bake one cake
BakeStdDev time.Duration // standard deviation of baking time
BakeBuf int // buffer slots between baking and icing
NumIcers int // number of cooks doing icing
IceTime time.Duration // time to ice one cake
IceStdDev time.Duration // standard deviation of icing time
IceBuf int // buffer slots between icing and inscribing
InscribeTime time.Duration // time to inscribe one cake
InscribeStdDev time.Duration // standard deviation of inscribing time
}
type cake int
func (s *Shop) baker(baked chan<- cake) {
for i := 0; i < s.Cakes; i++ {
c := cake(i)
if s.Verbose {
fmt.Println("baking", c)
}
work(s.BakeTime, s.BakeStdDev)
baked <- c
}
close(baked)
}
func (s *Shop) icer(iced chan<- cake, baked <-chan cake) {
for c := range baked {
if s.Verbose {
fmt.Println("icing", c)
}
work(s.IceTime, s.IceStdDev)
iced <- c
}
}
func (s *Shop) inscriber(iced <-chan cake) {
for i := 0; i < s.Cakes; i++ {
c := <-iced
if s.Verbose {
fmt.Println("inscribing", c)
}
work(s.InscribeTime, s.InscribeStdDev)
if s.Verbose {
fmt.Println("finished", c)
}
}
}
// Work runs the simulation 'runs' times.
func (s *Shop) Work(runs int) {
for run := 0; run < runs; run++ {
baked := make(chan cake, s.BakeBuf)
iced := make(chan cake, s.IceBuf)
go s.baker(baked)
for i := 0; i < s.NumIcers; i++ {
go s.icer(iced, baked)
}
s.inscriber(iced)
}
}
// work blocks the calling goroutine for a period of time
// that is normally distributed around d
// with a standard deviation of stddev.
func work(d, stddev time.Duration) {
delay := d + time.Duration(rand.NormFloat64()*float64(stddev))
time.Sleep(delay)
}
并发的循环
将循环的每个goroutine完成情况报告给外部的goroutine知晓,方式是向一个共享的channel中发送事件
// makeThumbnails3 makes thumbnails of the specified files in parallel.
func makeThumbnails3(filenames []string) {
ch := make(chan struct{})
for _, f := range filenames {
go func(f string) {
thumbnail.ImageFile(f) // NOTE: ignoring errors
ch <- struct{}{}
}(f)
}
// Wait for goroutines to complete.
for range filenames {
<-ch
}
}
注意我们将f的值作为一个显式的变量传给了函数,而不是在循环的闭包中声明:
for _, f := range filenames {
go func() {
thumbnail.ImageFile(f) // NOTE: incorrect!
// ...
}()
}
回忆一下之前在5.6.1节中,匿名函数中的循环变量快照问题。上面这个单独的变量f是被所有
的匿名函数值所共享,且会被连续的循环迭代所更新的。当新的goroutine开始执行字面函数
时,for循环可能已经更新了f并且开始了另一轮的迭代或者(更有可能的)已经结束了整个循
环,所以当这些goroutine开始读取f的值时,它们所看到的值已经是slice的最后一个元素了。
显式地添加这个参数,我们能够确保使用的f是当go语句执行时的“当前”那个f。
下一个版本的makeThumbnails使用了一个buffered channel来返回生成的图片文件的名字,
附带生成时的错误。
// makeThumbnails5 makes thumbnails for the specified files in parallel.
// It returns the generated file names in an arbitrary order,
// or an error if any step failed.
func makeThumbnails5(filenames []string) (thumbfiles []string, err error) {
type item struct {
thumbfile string
err error
}
ch := make(chan item, len(filenames))
for _, f := range filenames {
go func(f string) {
var it item
it.thumbfile, it.err = thumbnail.ImageFile(f)
ch <- it
}(f)
}
for range filenames {
it := <-ch
if it.err != nil {
return nil, it.err
}
thumbfiles = append(thumbfiles, it.thumbfile)
}
return thumbfiles, nil
}
sync.WaitGroup,一个递增的计数器,在每一个goroutine启动时加一,在goroutine退出时减一。这需要一种特 殊的计数器,这个计数器需要在多个goroutine操作时做到安全并且提供在其减为零之前一直 等待的一种方法。
// makeThumbnails6 makes thumbnails for each file received from the channel.
// It returns the number of bytes occupied by the files it creates.
func makeThumbnails6(filenames <-chan string) int64 {
sizes := make(chan int64)
var wg sync.WaitGroup // number of working goroutines
for f := range filenames {
wg.Add(1)
// worker
go func(f string) {
defer wg.Done()
thumb, err := thumbnail.ImageFile(f)
if err != nil {
log.Println(err)
return
}
info, _ := os.Stat(thumb) // OK to ignore error
sizes <- info.Size()
}(f)
}
// closer
go func() {
wg.Wait()
close(sizes)
}()
var total int64
for size := range sizes {
total += size
}
return total
}
注意Add和Done方法的不对称。Add是为计数器加一,必须在worker goroutine开始之前调 用,而不是在goroutine中;否则的话我们没办法确定Add是在"closer" goroutine调用Wait之前 被调用。并且Add还有一个参数,但Done却没有任何参数;其实它和Add(-1)是等价的。我们 使用defer来确保计数器即使是在出错的情况下依然能够正确地被减掉。上面的程序代码结构 是当我们使用并发循环,但又不知道迭代次数时很通常而且很地道的写法。
sizes channel携带了每一个文件的大小到main goroutine,在main goroutine中使用了range loop来计算总和。观察一下我们是怎样创建一个closer goroutine,并让其在所有worker goroutine们结束之后再关闭sizes channel的。两步操作:wait和close,必须是基于sizes的循 环的并发。考虑一下另一种方案:如果等待操作被放在了main goroutine中,在循环之前,这 样的话就永远都不会结束了,如果在循环之后,那么又变成了不可达的部分,因为没有任何 东西去关闭这个channel,这个循环就永远都不会终止。
下图表明了makethumbnails6函数中事件的序列。纵列表示goroutine。窄线段代表sleep, 粗线段代表活动。斜线箭头代表用来同步两个goroutine的事件。时间向下流动。注意main goroutine是如何大部分的时间被唤醒执行其range循环,等待worker发送值或者closer来关闭 channel的。
限制goroutine的启动数量
// tokens is a counting semaphore used to
// enforce a limit of 20 concurrent requests.
var tokens = make(chan struct{}, 20)
func crawl(url string) []string {
fmt.Println(url)
tokens <- struct{}{} // acquire a token
list, err := links.Extract(url)
<-tokens // release the token
if err != nil {
log.Print(err)
}
return list
}
select
time.After函数会立即返回一个channel,并起一个新的 goroutine在经过特定的时间后向该channel发送一个独立的值。下面的select语句会会一直等 待到两个事件中的一个到达,无论是abort事件或者一个10秒经过的事件。如果10秒经过了还 没有abort事件进入,那么火箭就会发射。
func main() {
// ...create abort channel...
fmt.Println("Commencing countdown. Press return to abort.")
select {
case <-time.After(10 * time.Second):
// Do nothing.
case <-abort:
fmt.Println("Launch aborted!")
return
}
launch()
}
如果多个case同时就绪时,select会随机地选择一个执行。
一个倒计时10秒的程序
func main() {
// ...create abort channel...
fmt.Println("Commencing countdown. Press return to abort.")
tick := time.Tick(1 * time.Second)
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
select {
case <-tick:
// Do nothing.
case <-abort: //其他发射前事故
fmt.Println("Launch aborted!")
return
}
}
launch()
}
一般的计时
ticker := time.NewTicker(1 * time.Second)
<-ticker.C // receive from the ticker's channel
ticker.Stop() // cause the ticker's goroutine to terminate
channel的零值是nil。也许会让你觉得比较奇怪,nil的channel有时候也是有一些用处的。因 为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不 会被select到。
select在goroutines中的应用方式:
go func() {
for {
select {
case job := <-w.JobQueue:
job.Do()
}
wq <- w.JobQueue //
}
}()
channel退出
channel的零值是nil。也许会让你觉得比较奇怪,nil的channel有时候也是有一些用处的。因 为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不 会被select到。并封装到一个公共函数
var done = make(chan struct{})
func cancelled() bool {
select {
case <-done:
return true
default:
return false
}
}
(channel的退出小节暂时略过