并发 您所在的位置:网站首页 go并发编程用什么模型 并发

并发

2024-07-12 01:28| 来源: 网络整理| 查看: 265

并发

​你可以在这里找到本章的所有代码​

这是我们的计划:同事已经写了一个 CheckWebsites 的函数检查 URL 列表的状态。

package concurrency type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { results[url] = wc(url) } return results }

它返回一个 map,由每个 url 检查后的得到的布尔值组成,成功响应的值为 true,错误响应的值为 false。

你还必须传入一个 WebsiteChecker 处理单个 URL 并返回一个布尔值。它会被函数调用以检查所有的网站。

使用 依赖注入,允许在不发起真实 HTTP 请求的情况下测试函数,这使测试变得可靠和快速。

这是他们写的测试:

package concurrency import ( "reflect" "testing" ) func mockWebsiteChecker(url string) bool { if url == "waat://furhurterwe.geds" { return false } return true } func TestCheckWebsites(t *testing.T) { websites := []string{ "http://google.com", "http://blog.gypsydave5.com", "waat://furhurterwe.geds", } want := map[string]bool{ "http://google.com": true, "http://blog.gypsydave5.com": true, "waat://furhurterwe.geds": false, } got := CheckWebsites(mockWebsiteChecker, websites) if !reflect.DeepEqual(want, got) { t.Fatalf("Wanted %v, got %v", want, got) } }

该功能在生产环境中被用于检查数百个网站。但是你的同事开始抱怨它速度很慢,所以他们请你帮忙为程序提速。

写一个测试

首先我们对 CheckWebsites 做一个基准测试,这样就能看到我们修改的影响。

package concurrency import ( "testing" "time" ) func slowStubWebsiteChecker(_ string) bool { time.Sleep(20 * time.Millisecond) return true } func BenchmarkCheckWebsites(b *testing.B) { urls := make([]string, 100) for i := 0; i < len(urls); i++ { urls[i] = "a url" } for i := 0; i < b.N; i++ { CheckWebsites(slowStubWebsiteChecker, urls) } }

基准测试使用一百个网址的 slice 对 CheckWebsites 进行测试,并使用 WebsiteChecker 的伪造实现。slowStubWebsiteChecker 故意放慢速度。它使用 time.Sleep 明确等待 20 毫秒,然后返回 true。

当我们运行基准测试时使用 go test -bench=. 命令 (如果在 Windows Powershell 环境下使用 go test -bench="."):

pkg: github.com/gypsydave5/learn-go-with-tests/concurrency/v0 BenchmarkCheckWebsites-4 1 2249228637 ns/op PASS ok github.com/gypsydave5/learn-go-with-tests/concurrency/v0 2.268s

CheckWebsite 经过基准测试的时间为 2249228637 纳秒,大约 2.25 秒。

让我们尝试去让它运行得更快。

编写足够的代码让它通过

现在我们终于可以谈论并发了,以下内容是为了说明「不止一件事情正在进行中」。这是我们每天很自然在做的事情。

比如,今天早上我泡了一杯茶。我放上水壶,然后在等待它煮沸时,从冰箱里取出了牛奶,把茶从柜子里拿出来,找到我最喜欢的杯子,把茶袋放进杯子里,然后等水壶沸了,把水倒进杯子里。

我 没有 做的事情是放上水壶,然后呆呆地盯着水壶等水煮沸,然后在煮沸后再做其他事情。

如果你能理解为什么第一种方式泡茶更快,那你就可以理解我们如何让 CheckWebsites 变得更快。与其等待网站响应之后再发送下一个网站的请求,不如告诉计算机在等待时就发起下一个请求。

通常在 Go 中,当调用函数 doSomething() 时,我们等待它返回(即使它没有值返回,我们仍然等待它完成)。我们说这个操作是 阻塞 的 —— 它让我们等待它完成。Go 中不会阻塞的操作将在称为 goroutine 的单独 进程 中运行。将程序想象成从上到下读 Go 的 代码,当函数被调用执行读取操作时,进入每个函数「内部」。当一个单独的进程开始时,就像开启另一个 reader(阅读程序)在函数内部执行读取操作,原来的 reader 继续向下读取 Go 代码。

要告诉 Go 开始一个新的 goroutine,我们把一个函数调用变成 go 声明,通过把关键字 go 放在它前面:go doSomething()。

package concurrency type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { go func() { results[url] = wc(url) }() } return results }

因为开启 goroutine 的唯一方法就是将 go 放在函数调用前面,所以当我们想要启动 goroutine 时,我们经常使用 匿名函数(anonymous functions)。一个匿名函数文字看起来和正常函数声明一样,但没有名字(意料之中)。你可以在 上面的 for 循环体中看到一个。

匿名函数有许多有用的特性,其中两个上面正在使用。首先,它们可以在声明的同时执行 —— 这就是匿名函数末尾的 () 实现的。其次,它们维护对其所定义的词汇作用域的访问权 —— 在声明匿名函数时所有可用的变量也可在函数体内使用。

上面匿名函数的主体和之前循环体中的完全一样。唯一的区别是循环的每次迭代都会启动一个新的 goroutine,与当前进程(WebsiteChecker 函数)同时发生,每个循环都会将结果添加到 results map 中。

但是当我们执行 go test:

--- FAIL: TestCheckWebsites (0.00s) CheckWebsites_test.go:31: Wanted map[http://google.com:true http://blog.gypsydave5.com:true waat://furhurterwe.geds:false], got map[] FAIL exit status 1 FAIL github.com/gypsydave5/learn-go-with-tests/concurrency/v1 0.010s 快速进入平行宇宙......

你可能不会得到这个结果。你可能会得到一个 panic 信息,这个稍后再谈。如果你得到的是那些结果,不要担心,只要继续运行测试,直到你得到上述结果。或假装你得到了,这取决于你。欢迎来到并发编程的世界:如果处理不正确,很难预测会发生什么。别担心 —— 这就是我们编写测试的原因,当处理并发时,测试帮助我们预测可能发生的情况。

... 重新回到这些问题。

让我们困惑的是,原来的测试 WebsiteChecker 现在返回空的 map。哪里出问题了?

我们 for 循环开始的 goroutines 没有足够的时间将结果添加结果到 results map 中;WebsiteChecker 函数对于它们来说太快了,以至于它返回时仍为空的 map。

为了解决这个问题,我们可以等待所有的 goroutine 完成他们的工作,然后返回。两秒钟应该能完成了,对吧?

package concurrency import "time" type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { go func() { results[url] = wc(url) }() } time.Sleep(2 * time.Second) return results }

现在当我们运行测试时获得的结果(如果没有得到 —— 参考上面的做法):

--- FAIL: TestCheckWebsites (0.00s) CheckWebsites_test.go:31: Wanted map[http://google.com:true http://blog.gypsydave5.com:true waat://furhurterwe.geds:false], got map[waat://furhurterwe.geds:false] FAIL exit status 1 FAIL github.com/gypsydave5/learn-go-with-tests/concurrency/v1 0.010s

这不是很好 - 为什么只有一个结果?我们可以尝试通过增加等待的时间来解决这个问题 —— 如果你愿意,可以试试。但没什么作用。这里的问题是变量 url 被重复用于 for 循环的每次迭代 —— 每次都会从 urls 获取新值。但是我们的每个 goroutine 都是 url 变量的引用 —— 它们没有自己的独立副本。所以他们 都 会写入在迭代结束时的 url —— 最后一个 url。这就是为什么我们得到的结果是最后一个 url。

解决这个问题:

package concurrency import ( "time" ) type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { go func(u string) { results[u] = wc(u) }(url) } time.Sleep(2 * time.Second) return results }

通过给每个匿名函数一个参数 url(u),然后用 url 作为参数调用匿名函数,我们确保 u 的值固定为循环迭代的 url 值,重新启动 goroutine。u 是 url 值的副本,因此无法更改。

现在,如果你幸运的话,你会得到:

PASS ok github.com/gypsydave5/learn-go-with-tests/concurrency/v1 2.012s

但是,如果你不走运(如果你运行基准测试,这很可能会发生,因为你将发起多次的尝试)。

fatal error: concurrent map writes goroutine 8 [running]: runtime.throw(0x12c5895, 0x15) /usr/local/Cellar/go/1.9.3/libexec/src/runtime/panic.go:605 +0x95 fp=0xc420037700 sp=0xc4200376e0 pc=0x102d395 runtime.mapassign_faststr(0x1271d80, 0xc42007acf0, 0x12c6634, 0x17, 0x0) /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:783 +0x4f5 fp=0xc420037780 sp=0xc420037700 pc=0x100eb65 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1(0xc42007acf0, 0x12d3938, 0x12c6634, 0x17) /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x71 fp=0xc4200377c0 sp=0xc420037780 pc=0x12308f1 runtime.goexit() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/asm_amd64.s:2337 +0x1 fp=0xc4200377c8 sp=0xc4200377c0 pc=0x105cf01 created by github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xa1 ... many more scary lines of text ...

这看上去冗长、可怕,我们需要深呼吸并阅读错误:fatal error: concurrent map writes。有时候,当我们运行我们的测试时,两个 goroutines 完全同时写入 results map。Go 的 Maps 不喜欢多个事物试图一次性写入,所以就导致了 fatal error。

这是一种 race condition(竞争条件),当软件的输出取决于事件发生的时间和顺序时,因为我们无法控制,bug 就会出现。因为我们无法准确控制每个 goroutine 写入结果 map 的时间,两个 goroutines 同一时间写入时程序将非常脆弱。

Go 可以帮助我们通过其内置的 race detector 来发现竞争条件。要启用此功能,请使用 race 标志运行测试:go test -race。

你应该得到一些如下所示的输出:

================== WARNING: DATA RACE Write at 0x00c420084d20 by goroutine 8: runtime.mapassign_faststr() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:774 +0x0 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x82 Previous write at 0x00c420084d20 by goroutine 7: runtime.mapassign_faststr() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:774 +0x0 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x82 Goroutine 8 (running) created at: github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xc4 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.TestWebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker_test.go:27 +0xad testing.tRunner() /usr/local/Cellar/go/1.9.3/libexec/src/testing/testing.go:746 +0x16c Goroutine 7 (finished) created at: github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xc4 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.TestWebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker_test.go:27 +0xad testing.tRunner() /usr/local/Cellar/go/1.9.3/libexec/src/testing/testing.go:746 +0x16c ==================

细节还是难以阅读 - 但 WARNING: DATA RACE 相当明确。阅读错误的内容,我们可以看到两个不同的 goroutines 在 map 上执行写入操作:

`Write at 0x00c420084d20 by goroutine 8:`

正在写入相同的内存块

`Previous write at 0x00c420084d20 by goroutine 7:`

最重要的是,我们可以看到发生写入的代码行:

`/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12`

和 goroutines 7 和 8 开始的代码行号:

`/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11`

你需要知道的所有内容都会打印到你的终端上 - 你只需耐心阅读就可以了。

Channels

我们可以通过使用 channels 协调我们的 goroutines 来解决这个数据竞争。channels 是一个 Go 数据结构,可以同时接收和发送值。这些操作以及细节允许不同进程之间的通信。

在这种情况下,我们想要考虑父进程和每个 goroutine 之间的通信,goroutine 使用 url 来执行 WebsiteChecker 函数。

package concurrency type WebsiteChecker func(string) bool type result struct { string bool } func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) resultChannel := make(chan result) for _, url := range urls { go func(u string) { resultChannel


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有