并发

Go 将并发结构作为核心语言的一部分提供。本节课程通过一些示例介绍并展示了它们的用法。

Go 作者组编写,Go-zh 小组翻译。

https://tour.go-zh.org/concurrency/1

  • Go 程

Go 程(goroutine)是由 Go 运行时管理的轻量级线程。

  1. go f(x, y, z)

会启动一个新的 Go 程并执行

  1. f(x, y, z)

f, x, yz 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。

Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。[[https://go-zh.org/pkg/sync/][sync]] 包提供了这种能力,不过在 Go 中并不经常用到,因为还有其它的办法(见下一页)。

  1. // +build OMIT
  2. package main
  3. import (
  4. "fmt"
  5. "time"
  6. )
  7. func say(s string) {
  8. for i := 0; i < 5; i++ {
  9. time.Sleep(100 * time.Millisecond)
  10. fmt.Println(s)
  11. }
  12. }
  13. func main() {
  14. go say("world")
  15. say("hello")
  16. }
  • 信道

信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。

  1. ch <- v // 将 v 发送至信道 ch。
  2. v := <-ch // 从 ch 接收值并赋予 v。

(“箭头”就是数据流的方向。)

和映射与切片一样,信道在使用前必须创建:

  1. ch := make(chan int)

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

以下示例对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。

  1. // +build OMIT
  2. package main
  3. import "fmt"
  4. func sum(s []int, c chan int) {
  5. sum := 0
  6. for _, v := range s {
  7. sum += v
  8. }
  9. c <- sum // 将和送入 c
  10. }
  11. func main() {
  12. s := []int{7, 2, 8, -9, 4, 0}
  13. c := make(chan int)
  14. go sum(s[:len(s)/2], c)
  15. go sum(s[len(s)/2:], c)
  16. x, y := <-c, <-c // 从 c 中接收
  17. fmt.Println(x, y, x+y)
  18. }
  • 带缓冲的信道

信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:

  1. ch := make(chan int, 100)

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

修改示例填满缓冲区,然后看看会发生什么。

  1. // +build OMIT
  2. package main
  3. import "fmt"
  4. func main() {
  5. ch := make(chan int, 2)
  6. ch <- 1
  7. ch <- 2
  8. fmt.Println(<-ch)
  9. fmt.Println(<-ch)
  10. }
  • range 和 close

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

  1. v, ok := <-ch

此时 ok 会被设置为 false

循环 fori:=rangec 会不断从信道接收值,直到它被关闭。

注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。

还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

  1. // +build OMIT
  2. package main
  3. import (
  4. "fmt"
  5. )
  6. func fibonacci(n int, c chan int) {
  7. x, y := 0, 1
  8. for i := 0; i < n; i++ {
  9. c <- x
  10. x, y = y, x+y
  11. }
  12. close(c)
  13. }
  14. func main() {
  15. c := make(chan int, 10)
  16. go fibonacci(cap(c), c)
  17. for i := range c {
  18. fmt.Println(i)
  19. }
  20. }
  • select 语句

select 语句使一个 Go 程可以等待多个通信操作。

select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

  1. // +build OMIT
  2. package main
  3. import "fmt"
  4. func fibonacci(c, quit chan int) {
  5. x, y := 0, 1
  6. for {
  7. select {
  8. case c <- x:
  9. x, y = y, x+y
  10. case <-quit:
  11. fmt.Println("quit")
  12. return
  13. }
  14. }
  15. }
  16. func main() {
  17. c := make(chan int)
  18. quit := make(chan int)
  19. go func() {
  20. for i := 0; i < 10; i++ {
  21. fmt.Println(<-c)
  22. }
  23. quit <- 0
  24. }()
  25. fibonacci(c, quit)
  26. }
  • 默认选择

select 中的其它分支都没有准备好时,default 分支就会执行。

为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:

  1. select {
  2. case i := <-c:
  3. // 使用 i
  4. default:
  5. // 从 c 中接收会阻塞时执行
  6. }
  1. // +build OMIT
  2. package main
  3. import (
  4. "fmt"
  5. "time"
  6. )
  7. func main() {
  8. tick := time.Tick(100 * time.Millisecond)
  9. boom := time.After(500 * time.Millisecond)
  10. for {
  11. select {
  12. case <-tick:
  13. fmt.Println("tick.")
  14. case <-boom:
  15. fmt.Println("BOOM!")
  16. return
  17. default:
  18. fmt.Println(" .")
  19. time.Sleep(50 * time.Millisecond)
  20. }
  21. }
  22. }
  • 练习:等价二叉查找树

不同二叉树的叶节点上可以保存相同的值序列。例如,以下两个二叉树都保存了序列 1,1,2,3,5,8,13

.image /content/img/tree.png

在大多数语言中,检查两个二叉树是否保存了相同序列的函数都相当复杂。

我们将使用 Go 的并发和信道来编写一个简单的解法。

本例使用了 tree 包,它定义了类型:

  1. type Tree struct {
  2. Left *Tree
  3. Value int
  4. Right *Tree
  5. }

点击[[javascript:click('.next-page')][下一页]]继续。

  • 练习:等价二叉查找树

1. 实现 Walk 函数。

2. 测试 Walk 函数。

函数 tree.New(k) 用于构造一个随机结构的已排序二叉查找树,它保存了值 k, 2k, 3k, ..., 10k

创建一个新的信道 ch 并且对其进行步进:

  1. go Walk(tree.New(1), ch)

然后从信道中读取并打印 10 个值。应当是数字 1,2,3,...,10

3.Walk 实现 Same 函数来检测 t1t2 是否存储了相同的值。

4. 测试 Same 函数。

Same(tree.New(1),tree.New(1))应当返回true,而 Same(tree.New(1),tree.New(2)) 应当返回 false

Tree 的文档可在[[https://godoc.org/golang.org/x/tour/tree#Tree][这里]]找到。

  1. // +build no-build OMIT
  2. package main
  3. import "golang.org/x/tour/tree"
  4. // Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
  5. func Walk(t *tree.Tree, ch chan int)
  6. // Same 检测树 t1 和 t2 是否含有相同的值。
  7. func Same(t1, t2 *tree.Tree) bool
  8. func main() {
  9. }
  • sync.Mutex

我们已经看到信道非常适合在各个 Go 程间进行通信。

但是如果我们并不需要通信呢?比如说,若我们只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?

这里涉及的概念叫做 互斥(mutualexclusion)* ,我们通常使用 互斥锁(Mutex) 这一数据结构来提供这种机制。

Go 标准库中提供了 [[https://go-zh.org/pkg/sync/#Mutex][sync.Mutex]] 互斥锁类型及其两个方法:

  • Lock
  • Unlock

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。参见 Inc 方法。

我们也可以用 defer 语句来保证互斥锁一定会被解锁。参见 Value 方法。

  1. // +build OMIT
  2. package main
  3. import (
  4. "fmt"
  5. "sync"
  6. "time"
  7. )
  8. // SafeCounter 的并发使用是安全的。
  9. type SafeCounter struct {
  10. v map[string]int
  11. mux sync.Mutex
  12. }
  13. // Inc 增加给定 key 的计数器的值。
  14. func (c *SafeCounter) Inc(key string) {
  15. c.mux.Lock()
  16. // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
  17. c.v[key]++
  18. c.mux.Unlock()
  19. }
  20. // Value 返回给定 key 的计数器的当前值。
  21. func (c *SafeCounter) Value(key string) int {
  22. c.mux.Lock()
  23. // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
  24. defer c.mux.Unlock()
  25. return c.v[key]
  26. }
  27. func main() {
  28. c := SafeCounter{v: make(map[string]int)}
  29. for i := 0; i < 1000; i++ {
  30. go c.Inc("somekey")
  31. }
  32. time.Sleep(time.Second)
  33. fmt.Println(c.Value("somekey"))
  34. }
  • 练习:Web 爬虫

在这个练习中,我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。

修改 Crawl 函数来并行地抓取 URL,并且保证不重复。

提示:你可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!

  1. // +build OMIT
  2. package main
  3. import (
  4. "fmt"
  5. )
  6. type Fetcher interface {
  7. // Fetch 返回 URL 的 body 内容,并且将在这个页面上找到的 URL 放到一个 slice 中。
  8. Fetch(url string) (body string, urls []string, err error)
  9. }
  10. // Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
  11. func Crawl(url string, depth int, fetcher Fetcher) {
  12. // TODO: 并行的抓取 URL。
  13. // TODO: 不重复抓取页面。
  14. // 下面并没有实现上面两种情况:
  15. if depth <= 0 {
  16. return
  17. }
  18. body, urls, err := fetcher.Fetch(url)
  19. if err != nil {
  20. fmt.Println(err)
  21. return
  22. }
  23. fmt.Printf("found: %s %q\n", url, body)
  24. for _, u := range urls {
  25. Crawl(u, depth-1, fetcher)
  26. }
  27. return
  28. }
  29. func main() {
  30. Crawl("https://golang.org/", 4, fetcher)
  31. }
  32. // fakeFetcher 是返回若干结果的 Fetcher。
  33. type fakeFetcher map[string]*fakeResult
  34. type fakeResult struct {
  35. body string
  36. urls []string
  37. }
  38. func (f fakeFetcher) Fetch(url string) (string, []string, error) {
  39. if res, ok := f[url]; ok {
  40. return res.body, res.urls, nil
  41. }
  42. return "", nil, fmt.Errorf("not found: %s", url)
  43. }
  44. // fetcher 是填充后的 fakeFetcher。
  45. var fetcher = fakeFetcher{
  46. "https://golang.org/": &fakeResult{
  47. "The Go Programming Language",
  48. []string{
  49. "https://golang.org/pkg/",
  50. "https://golang.org/cmd/",
  51. },
  52. },
  53. "https://golang.org/pkg/": &fakeResult{
  54. "Packages",
  55. []string{
  56. "https://golang.org/",
  57. "https://golang.org/cmd/",
  58. "https://golang.org/pkg/fmt/",
  59. "https://golang.org/pkg/os/",
  60. },
  61. },
  62. "https://golang.org/pkg/fmt/": &fakeResult{
  63. "Package fmt",
  64. []string{
  65. "https://golang.org/",
  66. "https://golang.org/pkg/",
  67. },
  68. },
  69. "https://golang.org/pkg/os/": &fakeResult{
  70. "Package os",
  71. []string{
  72. "https://golang.org/",
  73. "https://golang.org/pkg/",
  74. },
  75. },
  76. }
  • 接下来去哪?

Go[[https://go-zh.org/doc/][文档]]是一个极好的开始。

它包含了参考、指南、视频等等更多资料。

了解如何组织 Go 代码并在其上工作,参阅[[https://www.youtube.com/watch?v=XCsL89YtqCs][此视频]],或者阅读[[https://go-zh.org/doc/code.html][如何编写 Go 代码]]。

如果你需要标准库方面的帮助,请参考[[https://go-zh.org/pkg/][包手册]]。如果是语言本身的帮助,阅读[[https://go-zh.org/ref/spec][语言规范]]是件令人愉快的事情。

进一步探索 Go 的并发模型,参阅 [https://www.youtube.com/watch?v=f6kdp27TYZs][Go 并发模型]以及[https://www.youtube.com/watch?v=QDDwwePbDtw][深入 Go 并发模型]并阅读[[https://go-zh.org/doc/codewalk/sharemem/][通过通信共享内存]]的代码之旅。

想要开始编写 Web 应用,请参阅[https://vimeo.com/53221558][一个简单的编程环境]并阅读[[https://go-zh.org/doc/articles/wiki/][编写 Web 应用]]的指南。

[[https://go-zh.org/doc/codewalk/functions/][函数:Go 中的一等公民]]展示了有趣的函数类型。

[[https://blog.go-zh.org/][Go 博客]]有着众多关于 Go 的文章和信息。

[[https://www.mikespook.com/tag/golang/][mikespook 的博客]]中有大量中文的关于 Go 的文章和翻译。

开源电子书 [[https://github.com/astaxie/build-web-application-with-golang][Go Web 编程]]和 [[https://github.com/Unknwon/the-way-to-go_ZH_CN][Go 入门指南]]能够帮助你更加深入的了解和学习 Go 语言。

访问 [[https://go-zh.org][go-zh.org]] 了解更多内容。

Go语言_并发的更多相关文章

  1. [日常] GO语言圣经-并发获取多个URL

    go语言圣经-并发获取多个URL 1.GO最新奇的特性就是对并发编程的支持,goroutine和channel 2.goroutine是一种函数的并发执行方式,而channel是用来在goroutin ...

  2. 怎样让Oracle支持中文? 语言_地域.字符集

    暂时不涉及数据库业务,但是今天入库的时候中文入库报错,考虑可能是字体不支持,留待备用. 来源:Linux社区  作者:robertkun 语言_地域.字符集SIMPLIFIED CHINESE_CHI ...

  3. c语言_常见图片格式判断

    c语言_常见图片格式判断 我想尽各种思路.今天,终于把图片判断搞定了. 在此,我写一下我的思路.希望对那些不想看代码的朋友们有帮助. 常风的的图片格式有:bmp,png,jpg,gif等图片格式. 我 ...

  4. BZOJ_1212_[HNOI2004]L语言_哈希

    BZOJ_1212_[HNOI2004]L语言_哈希 Description 标点符号的出现晚于文字的出现,所以以前的语言都是没有标点的.现在你要处理的就是一段没有标点的文章. 一段文章T是由若干小写 ...

  5. Go语言的并发

    一.Go语言中Goroutine的基本原理 Go语言里的并发指的是能让某个函数独立于其他函数运行的能力. Go语言的goroutine是一个独立的工作单元, Go 语言的并发同步模型来自一个叫作通信顺 ...

  6. 选择排序_C语言_数组

    选择排序_C语言_数组 #include <stdio.h> void select_sort(int *); int main(int argc, const char * argv[] ...

  7. 插入排序_C语言_数组

    插入排序_C语言_数组 #include <stdio.h> void insertSort(int *); int main(int argc, const char * argv[]) ...

  8. 快速排序_C语言_数组

    快速排序_C语言_数组 #include <stdio.h> void quickSort(int *, int, int); int searchPos(int *, int, int) ...

  9. 冒泡排序_C语言_数组

    冒泡排序_C语言_数组 #include <stdio.h> //冒泡排序 小->大 void sort(int * pArray, int len); int main(int a ...

随机推荐

  1. js函数总结

    最近要经常写一些Js代码,总看到同事能使用js高级函数写出比较简洁的js代码,挺羡慕的,于是就花了一些专门时间来学习. forEach.map.reduce 我就不喜欢一上来就给出语法来,先来一个例子 ...

  2. django之路由的理解

    一:路由 简单的路由过程图: 1. 路由的定义位置 路由定义方式一:主路由和子路由分开定义 主路由的定义 urls.py from django.conf.urls import url from d ...

  3. 为啥HashMap的默认容量是16

    集合是Java开发日常开发中经常会使用到的,而作为一种典型的K-V结构的数据结构,HashMap对于Java开发者一定不陌生. 在日常开发中,我们经常会像如下方式以下创建一个HashMap: Map& ...

  4. TCP TIME_WAIT和CLOSE_WAIT

    原创转载请注明出处:https://www.cnblogs.com/agilestyle/p/11484451.html 使用如下指令查看当前Server的TCP状态 netstat -n | awk ...

  5. 论云时代最经济的APM工具的姿势

    阿里云于大概两月前商业化了一款APM产品 ARMS ,正式填补了 APM 上的云上监控的空白.那么作为阿里云官方 APM 工具,ARMS 和其他传统厂商的 APM 服务相比有什么特点呢? 通过和国内其 ...

  6. 01 安装IDEA

    https://www.jetbrains.com 1 . 2

  7. Vue的思考扩展

    1.Vue是如何实现数据双向绑定的 1.1.实现双向绑定的基本原理 数据驱动:Vue会通过Dircetives指令,对DOM做一层封装,当数据发生改变会通知指令去修改对应的DOM,数据驱动DOM变化, ...

  8. 从Word文档粘贴内容至Web编辑器的问题

    Chrome+IE默认支持粘贴剪切板中的图片,但是我要发布的文章存在word里面,图片多达数十张,我总不能一张一张复制吧?Chrome高版本提供了可以将单张图片转换在BASE64字符串的功能.但是无法 ...

  9. 黑苹果 MacOS 10.15 Catalina安装教程

    10.15 Catalina 桌面 一.准备工作 一个8G以上的U盘(有的U盘标的是8G,实际只有7.X,实际容量小于7.5G的会失败) MacOS镜像.TransMac(刻录工具).DiskGeni ...

  10. Linux下安装Tomcat(2)

    Tomcat是一个免费的开源的Serlvet容器,它是Apache基金会的Jakarta项目中的一个核心项目,由Apache,Sun和 其它一些公司及个人共同开发而成.由于有了Sun的参与和支持,最新 ...