Concurrency
目标功能:
type WebsiteChecker func(string) bool//输入单个网址,返回状态
func CheckWebsites(WebsiteChecker, []string) map[string]bool {}//输入网址切片,返回状态map
使用依赖注入dependency injection
测试函数的同时避免进行实际的http calls,测试调用CheckWebsites
函数时传入的WebsiteChecker
使用下面自己定义的函数mockWebsiteChecker
。在这里依赖注入使得我们的CheckWebsites
函数不再依赖于某个固定的WebsiteChecker
,mocking使得我们不需要真正地去访问这些网址,但是依然能够监视函数WebsiteChecker
函数是否被正确调用。
func mockWebsiteChecker(url string) bool {
if url == "waat://furhurterwe.geds" {
return false
}
return true
}
基准测试bench mark
为了测试函数性能,使用benchmark test基准测试。这里同样首先定制一个有固定时延的WebsiteChecker
依赖,将它注入到基准测试中。在这个定制的依赖中,我们不需要知道输入的是什么网站,只关心函数性能问题。因此假定判断每个网站状态需要的时间为20毫秒。
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"
}
b.ResetTimer()//这一步很重要,我们只测试CheckWebsites的性能所以需要reset时间
for i := 0; i < b.N; i++ {
CheckWebsites(slowStubWebsiteChecker, urls)
}
}
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
并发concurrency
concurrency —— having more than one thing in progress
Instead of waiting for a website to respond before sending a request to the next website, we will tell our computer to make the next request while it is waiting.
并发不是指同时干很多事情,而是指在某件事的进行过程中利用余裕去干其他事情。很多事情同时进行是并行。
- 阻塞 blocking:指调用某个函数之后等待它返回或结束执行的过程
- 协程 goroutine:在Go中不会阻塞的操作,在独立的进程中运行。(进程可以理解为从上到下执行Go代码的过程,每调用一个函数就进入并且执行该函数的代码;每当一个独立的进程开始,就相当于一个新的执行者在不打断之前执行者执行代码的同时,开始独立的执行过程)
- 开启goroutine的方法:
go 函数名(参数)
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
}
匿名函数
第9行使用了匿名函数作为goroutine的执行者。匿名函数具有两个有用的特性:
- 可以同时进行声明和执行,第11行的
()
就是在执行函数 - 可以在函数体内部直接地址引用所有当前环境下的变量。如果你想值引用这些变量的话可以将它的类型写到形参列表里,再在
()
中取用即可
Welcome to concurrency: when it’s not handled correctly it’s hard to predict what’s going to happen. 当我们再次go test
,会发现程序运行出错了,我们并没有得到我们想要的网站状态map。这是因为
- 我们的主程序的进程在开启三个goroutine之后并没有等待它们执行完毕就继续执行下去,导致还没来得及将所有
wc(url)
结果赋值给results[url]
就过早地进行结果判断 - 我们没有一个很好的机制控制三个goroutine按照次序进行赋值。在这个过程中可能会发生panic,因为两个goroutine同时对map进行写操作产生了race condition
- 还有一个可能出错的地方在于url以地址传递的方式传入匿名函数,在并发执行三个goroutine的过程中,有可能第一个goroutine还没做完,url已经变成了第三个网址,这就导致最终map里只存储了最后一个网址的状态。
解决方法:传入形参(值传递网址变量)、使用channel协调进程间通信
channels 是一个 Go 数据结构,可以同时接收和发送值。这些操作以及细节允许不同进程之间的通信。在这种情况下,我们想要考虑父进程和每个 goroutine 之间的通信。channel遵循==先入先出==原则。
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 <- result{u, wc(u)}
}(url)
}
for i := 0; i < len(urls); i++ {
result := <-resultChannel
results[result.string] = result.bool
}
return results
}
// Send statement
resultChannel <- result{u, wc(u)}
// Receive expression
r := <-resultChannel
pkg: github.com/gypsydave5/learn-go-with-tests/concurrency/v2
BenchmarkCheckWebsites-8 100 23406615 ns/op
PASS
ok github.com/gypsydave5/learn-go-with-tests/concurrency/v2 2.377s
总结
- goroutines 是 Go 的基本并发单元,它让我们可以同时检查多个网站。
- anonymous functions(匿名函数),我们用它来启动每个检查网站的并发进程。
- channels,用来组织和控制不同进程之间的交流,使我们能够避免 race condition(竞争条件) 的问题。
- the race detector(竞争探测器) 帮助我们调试并发代码的问题。
过早的优化是万恶之源,我们应当先确保程序的正确性再考虑加速