Go语言学习笔记 - 第八章 Goroutines和Channels(The Go Programming Language)

第八章 Goroutines和Channels

  • Go语言中的并发程序可以用两种手段来实现
    • goroutine和channel,其支持“顺序通信进程”(communicating sequential processes)或被简称为CSP。
    • 多线程共享内存。

8.1Goroutines

划重点

  • 在Go语言中,每一个并发的执行单元叫作一个goroutine
  • 主函数中的goroutine,叫做main goroutine
  • 创建goroutine,使用一个普通的函数或方法调用前加上关键字go
  • 主函数返回时,所有的goroutine都会被直接打断,程序退出
  • 除了主函数退出或者直接终止程序,还可以通过goroutine通信实现goroutine结束

常用库及方法

  • time.Millisecond time.Duration time.Sleep ``

8.2示例: 并发的Clock服务

划重点

常用库及方法

  • net.Listen net.Listen.Accept net.Conn net.Dial
  • time.Now().Format time.Sleep time.RFC1123 time.Parse

8.3示例: 并发的Echo服务

划重点

  • 使用go关键词的同时,需要慎重地考虑goroutine间传递的方法在并发地调用时是否安全,事实上对于大多数类型来说也确实不安全

常用库及方法

  • strings.ToUpper strings.ToLower
  • bufio.NewScanner input.Scan input.Text

8.4Channels

划重点

  • channels Go语言goroutine之间的通信机制
  • 一个channels是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。
  • 每个channel都有一个特殊的类型,一个可以发送int类型数据的channel一般写为chan int。
  • 使用内置的make函数,我们可以创建一个channel:
ch := make(chan int) // ch has type 'chan int'
  • 和map类似,channel也一个对应make创建的底层数据结构的引用,channel的零值也是nil
  • 当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象
  • 两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相通的对象,那么比较的结果为真。一个channel也可以和nil进行比较。
  • 一个channel有发送和接收两个主要操作,发送和接收两个操作都是用 <- 运算符。
  • 发送:ch <- x 接收: x = <- ch; 一个不使用接收结果的接收操作也是合法的
ch <- x // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch // a receive statement; result is discarded
  • Channel还支持close操作close(ch),用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。对于已经close掉的channel还可以接收之前已经成功发送的数据,没有数据则产生一个零值数据。
  • make创建的channel的第二个整形参数代表channel的容量,容量大于零表示带缓冲的channel。
ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3

8.4.1不带缓存的Channels

划重点

  • 无缓存的channel将导致发送者goroutine阻塞,直到被接收;反之,如果接收者先发生,接收者的goroutine会被阻塞,直到收到另一个goroutine发送的内容;
  • 基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作,无缓存Channels有时候也被称为同步Channels
  • 当通过一个无缓存Channels发送数据时,接收者收到数据发生在唤醒发送者goroutine之前,叫做,happens before
  • go语句调用了一个函数字面量,这Go语言中启动goroutine常用的形式。
  • 基于channels发送消息有两个重要方面:
    • 关心消息本身的值;
    • 关心消息发送的时刻,当强调通讯发生的时刻时,称为消息事件;
  • 对于不携带额外信息的消息事件,可以用struct{} 空结构体作为channels元素的类型,当然也可以使用bool或int类型实现同样的功能

常用库及方法

  • net.ResolveTCPAddr net.DialTCP

8.4.2串联的Channels(Pipeline)

划重点

  • 多个goroutine串联的Channels就是所谓的管道(pipeline);
  • 通知接收者没有多余值,可以通过close(channelName)来实现,当channel关闭后,再向该channel发送数据会导致panic,接收者将不再被阻塞,会返回一个零值。如果循环接收的话,将会接收无休止的零值。
  • 通过x, ok := <-channelName可以判断channel是否被关闭,接收成功返回true,channel关闭则返回false
  • Go语言的range循环可直接在channels上面迭代,当channel被关闭并且没有值可接收时跳出循环
  • 不管一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收
  • 试图重复关闭一个channel将导致panic异常,试图关闭一个nil值的channel也将导致panic异常。关闭一个channels还会触发一个广播机制

8.4.3单方向的Channel

划重点

  • 单方向的channel类型,分别用于只发送或只接收的channel。
  • 类型 chan<- int 表示一个只发送int的channel,类型 <-chan int 表示一个只接收int的channel。箭头 <- 和关键字chan的相对位置表明了channel的方向
  • 对一个只接收的channel调用close将是一个编译错误
  • 任何双向channel向单向channel变量的赋值操作都将导致隐
    式转换,反之不可以。

8.4.4带缓存的Channels

划重点

  • 带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。
  • ch = make(chan string, 3)表示创建了一个可以持有三个字符串元素的带缓存Channel。如图:
    image
  • 向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。也就是有缓存的channel可以理解为队列
  • 缓存channel满了将会阻塞发送goroutine,空了将会阻塞接收goroutine
  • 内置的cap函数可以获取channel的容量,内置的len函数可以获取channel的有效元素的个数;
  • 将一个带缓存的channel当作同一个goroutine中的队列使用是一个错误,简单的队列使用slice即可
  • 无缓存的channel导致没有人接收而被永远卡住,称为goroutine泄露,是一个BUG,泄漏的goroutines并不会被自动回收
  • 无缓存channel更强地保证了每个发送操作与相应的同步接收操作;但是对于带缓存channel,这些操作是解耦的。
  • Channel的缓存也可能影响程序的性能

8.5并发的循环

划重点

  • 注意for循环中的goroutine,由于一步会导致goroutine中的i不是想要赋予的变量值。i需要显示的传给goroutine,而不是在闭包中声明(循环变量快照问题):
    • 下面的调用是正确的;
    for _, f := range filenames {
    go func(f string) {
    thumbnail.ImageFile(f) // NOTE: ignoring errors
    ch <- struct{}{}
    }(f) // right
    }
    
    • 下面的调用是错误的;
    for _, f := range filenames {
    go func() {
    thumbnail.ImageFile(f) // NOTE: incorrect!
    // ...
    }()
    }
    
  • goroutine泄露的例子,这种情况。解决的方法是创建一个合适大小的buffered channel,或者创建另外一个goroutine,当main goroutine返回第一个错误的同时去排空其他的channel:
for _, f := range filenames {
  go func(f string) {
     _, err := thumbnail.ImageFile(f)
     errors <- err
     }(f)
}
for range filenames {
      if err := <-errors; err != nil {
      return err // NOTE: incorrect: goroutine leak!
      }
}
  • 在创建每个goroutine是计数器加一,退出是减一,并在减为零之前一直等待的计数器,被称为sync.WaitGroup

常用库及方法

  • image.Image image.Image.Bounds().Size().X image.Image.Bounds().Size().Y image.Rect image.NewRGBA image.Decode
  • jpeg.Encode
  • os.Open
  • filepath.Ext
  • strings.TrimSuffix
  • sync.WaitGroup sync.WaitGroup.Add sync.WaitGroup.Done sync.WaitGroup.Wait
  • runtime.GOMAXPROCS

8.6示例: 并发的Web爬虫

划重点

8.7基于select的多路复用

划重点

  • 多路复用(multiplex):
select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default:
// ...
}
  • select会等待case中有能够执行的case时去执行。当条件满足时,select才会去通信并执行case之后的语句;这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去。
  • 如果多个case同时就绪时,select会随机地选择一个执行
  • time.Tick是一个定时器,除非程序的整个生命周期都需要使用,否则要防止出现goroutine泄露,正确的用法如下:
ticker := time.NewTicker(1 * time.Second)
<-ticker.C // receive from the ticker's channel
ticker.Stop() // cause the ticker's goroutine to terminate
  • select会有一个default来设置当其它的操作都不能够马上被处理时程序需要执行哪些逻辑。可以让程序接收到channel的值,而不用完全阻塞。
  • “轮询channel”可以做到非阻塞的接收操作:
select {
case <-abort:
fmt.Printf("Launch aborted!\n")
return
default:
// do nothing
}
  • channel的零值是nil,nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到,所以可以使用nil来激活或者禁用case,来达成处理其它输入或输出事件时超时和取消的逻辑。

常用库及方法

  • time.Tick time.After time.NewTicker time.NewTimer timer.Reset timer.C
  • os.Stdin.Read

8.8示例: 并发的字典遍历

划重点

  • 标签break,break loop可以同时终结select和for两个循环
loop:
	for {
		select {
		case size, ok := <-fileSizes:
			if !ok {
				break loop // fileSizes was closed
			}
			nfiles++
			nbytes += size
		case <-tick:
			printDiskUsage(nfiles, nbytes)
		}
	}

常用库及方法

  • ioutil.ReadDir
  • os.FileInfo os.FileInfo.IsDir os.FileInfo.IsDir os.FileInfo.Size os.Stat
  • os.Stderr
  • filepath.Join
  • flag.Parse flag.Args flag.Bool
  • time.Tick
  • sync.WaitGroup

8.9并发的退出

划重点

  • Go语言并没有提供在一个goroutine中终止另一个goroutine的方法,由于这样会导致goroutine之间的共享变量落在未定义的状态上。
  • 一般情况下我们是很难知道在某一个时刻具体有多少个goroutine在运行着的。
  • 广播机制的实现:不要向channel发送值,而是用关闭一个channel来进行广播。来通知所有的需要abort channel 的goroutine退出。
  • os.Stdin.Read(make([]byte, 1))比较典型的连接到终端的程序。
  • 调用一个panic,然后runtime会把每一个goroutine的栈dump下来,这对于调试在main goroutine都返回之前是不是所有的goroutine都退出了很有用处。

常用库及方法

  • http.Request http.Get http.NewRequest http.DefaultClient.Do

8.10示例: 聊天服务

划重点

猜你喜欢

转载自blog.csdn.net/rabbit0206/article/details/103758497