#### gopl goroutines和channels ####

参考《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的退出小节暂时略过

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值