ch9-7: make loop

This commit is contained in:
chai2010
2016-01-18 17:47:12 +08:00
parent 1dcc27524c
commit 687549b05d

View File

@@ -1,8 +1,8 @@
## 9.7. 示例: 併發的非阻塞緩存
中我们会做一个无阻塞的存,这种工具可以助我们来解决现实世界中并发程序出现但没有现成的可以解决的问题。这个问题叫作存(memoizing)函数(译注Memoization的定 memoization 一是Donald Michie 根拉丁memorandum杜撰的一个词。相应的动词、过去分、ing形式有memoiz、memoized、memoizing.),也就是,我需要存函的返回结果,这样在对函数进行调用的候,我们就只需要一次算,之后只要返回计算的果就可以了。我的解方案会是并发安全且避免对整个缓存加锁而导致所有操作都去争一个锁的设计
中我們會做一個無阻塞的存,這種工具可以助我們來解決現實世界中併發程序出現但沒有現成的可以解決的問題。這個問題叫作存(memoizing)函數(譯註Memoization的定 memoization 一是Donald Michie 根拉丁memorandum杜撰的一個詞。相應的動詞、過去分、ing形式有memoiz、memoized、memoizing.),也就是,我需要存函的返迴結果,這樣在對函數進行調用的候,我們就隻需要一次算,之後隻要返迴計算的果就可以了。我的解方案會是併發安全且避免對整個緩存加鎖而導致所有操作都去爭一個鎖的設計
们将使用下面的httpGetBody函数作为我们需要存的函的一个样例。这个函数会去进行HTTP GET请求并且获取http响应body。对这个函数的调用本身开销是比大的,所以我们尽量尽量避免在不必要的候反复调用。
們將使用下面的httpGetBody函數作爲我們需要存的函的一個樣例。這個函數會去進行HTTP GET請求併且獲取http響應body。對這個函數的調用本身開銷是比大的,所以我們盡量盡量避免在不必要的候反複調用。
```go
func httpGetBody(url string) (interface{}, error) {
@@ -15,9 +15,9 @@ func httpGetBody(url string) (interface{}, error) {
}
```
一行稍微藏了一些细节。ReadAll会返回两个结果,一[]byte数组和一个错误,不过这两个对象可以被赋值给httpGetBody的返回声明里的interface{}和error型,所以我也就可以这样返回结果并且不需要外的工作了。我在httpGetBody中选用这种返回类型是了使其可以与缓存匹配。
一行稍微藏了一些細節。ReadAll會返迴兩個結果,一[]byte數組和一個錯誤,不過這兩個對象可以被賦值給httpGetBody的返迴聲明里的interface{}和error型,所以我也就可以這樣返迴結果併且不需要外的工作了。我在httpGetBody中選用這種返迴類型是了使其可以與緩存匹配。
下面是我们要设计的cache的第一“草稿”:
下面是我們要設計的cache的第一“草稿”:
```go
gopl.io/ch9/memo1
@@ -54,9 +54,9 @@ func (memo *Memo) Get(key string) (interface{}, error) {
}
```
Memo实例会记录需要存的函f(类型为Func),以及缓存内容(里面是一string到result映射的map)。每一result都是都是简单的函数返回的值对儿--一值和一个错误值。继续下去我们会展示一些Memo的变种,不所有的例子都遵循些上面的些方面。
Memo實例會記録需要存的函f(類型爲Func),以及緩存內容(里面是一string到result映射的map)。每一result都是都是簡單的函數返迴的值對兒--一值和一個錯誤值。繼續下去我們會展示一些Memo的變種,不所有的例子都遵循些上面的些方面。
下面是一使用Memo的例子。对于流入的URL的每一元素我们都会调用Get打印用延以及其返回的数据大小的log
下面是一使用Memo的例子。對於流入的URL的每一元素我們都會調用Get打印調用延以及其返迴的數據大小的log
```go
m := memo.New(httpGetBody)
@@ -71,7 +71,7 @@ for url := range incomingURLs() {
}
```
可以使用测试包(第11章的主题)来系统地鉴定缓存的效果。下面的测试输出,我可以看到URL流包含了一些重的情况,尽管我第一次每一URL的(\*Memo).Get的用都花上百毫秒,但第二次就需要花1毫秒就可以返完整的数据了。
可以使用測試包(第11章的主題)來繫統地鑒定緩存的效果。下面的測試輸出,我可以看到URL流包含了一些重的情況,盡管我第一次每一URL的(\*Memo).Get的調用都花上百毫秒,但第二次就需要花1毫秒就可以返完整的數據了。
```
$ go test -v gopl.io/ch9/memo1
@@ -89,9 +89,9 @@ PASS
ok gopl.io/ch9/memo1 1.257s
```
这个测试是顺序地去做所有的用的。
這個測試是順序地去做所有的調用的。
于这种彼此立的HTTP求可以很好地并发,我可以把这个测试改成并发形式。可以使用sync.WaitGroup等待所有的求都完成之再返
於這種彼此立的HTTP求可以很好地併發,我可以把這個測試改成併發形式。可以使用sync.WaitGroup等待所有的求都完成之再返
```go
m := memo.New(httpGetBody)
@@ -113,9 +113,9 @@ n.Wait()
```
这次测试跑起更快了,然而不幸的是貌似这个测试不是每次都能正常工作。我们注意到有一些意料之外的cache miss(存未命中),或者命中了存但却返回了错误的值,或者甚至直接崩
這次測試跑起更快了,然而不幸的是貌似這個測試不是每次都能正常工作。我們註意到有一些意料之外的cache miss(存未命中),或者命中了存但卻返迴了錯誤的值,或者甚至直接崩
但更糟糕的是,有时候这个程序是能正确的运行(:也就是最人崩的偶bug),所以我甚至可能都不会意识到这个程序有bug。。但是我可以使用-race这个flag来运行程序,竞争检测器(§9.6)打印像下面这样的报告:
但更糟糕的是,有時候這個程序是能正確的運行(:也就是最人崩的偶bug),所以我甚至可能都不會意識到這個程序有bug。。但是我可以使用-race這個flag來運行程序,競爭檢測器(§9.6)打印像下面這樣的報告:
```
$ go test -run=TestConcurrent -race -v gopl.io/ch9/memo1
@@ -138,7 +138,7 @@ Found 1 data race(s)
FAIL gopl.io/ch9/memo1 2.393s
```
memo.go的32行出现了两次,明有两个goroutine在有同步干预的情下更新了cache map。表明Get不是并发安全的,存在数据竞争
memo.go的32行出現了兩次,明有兩個goroutine在有同步榦預的情下更新了cache map。表明Get不是併發安全的,存在數據競爭
```go
28 func (memo *Memo) Get(key string) (interface{}, error) {
@@ -151,7 +151,7 @@ memo.go的32行出现了两次说明有两个goroutine在没有同步干预
35 }
```
简单的使cache并发安全的方式是使用基于监控的同步。只要给Memo加上一mutex在Get的一开始获取互斥return的时候释放锁,就可以cache的操作生在临界区内了:
簡單的使cache併發安全的方式是使用基於監控的同步。隻要給Memo加上一mutex在Get的一開始獲取互斥return的時候釋放鎖,就可以cache的操作生在臨界區內了:
```go
gopl.io/ch9/memo2
@@ -177,9 +177,9 @@ func (memo *Memo) Get(key string) (value interface{}, err error) {
}
```
测试依然并发进行,但这回竞争检查器“沉默”了。不幸的是对于Memo的这一点改变使我完全失了并发的性能优点。每次f的用期间都会持有Get将本来可以并行运行的I/O操作串行化了。我本章的目的是完成一个无锁缓存,而不是现在这样的将所有求串行化的函数的缓存。
測試依然併發進行,但這迴競爭檢査器“沉默”了。不幸的是對於Memo的這一點改變使我完全失了併發的性能優點。每次f的調用期間都會持有Get將本來可以併行運行的I/O操作串行化了。我本章的目的是完成一個無鎖緩存,而不是現在這樣的將所有求串行化的函數的緩存。
下一Get的实现,调用Get的goroutine会两次获取锁:查找阶段获取一次,如果查找没有返任何容,那么进入更新阶段会再次取。在这两次获取锁的中间阶其它goroutine可以意使用cache。
下一Get的實現,調用Get的goroutine會兩次獲取鎖:査找階段獲取一次,如果査找沒有返任何容,那麽進入更新階段會再次取。在這兩次獲取鎖的中間階其它goroutine可以意使用cache。
```go
gopl.io/ch9/memo3
@@ -200,9 +200,9 @@ func (memo *Memo) Get(key string) (value interface{}, err error) {
}
```
这些修改使性能再次得到了提但有一些URL被取了次。这种情况在两个以上的goroutine同一时刻调用Get来请求同的URL时会发生。多goroutine一起查询cache发现没有值,然一起用f这个慢不拉的函。在得到结果后,也都去去更新map。其中一个获得的结果会覆盖掉另一个的结果。
這些脩改使性能再次得到了提但有一些URL被取了次。這種情況在兩個以上的goroutine同一時刻調用Get來請求同的URL時會發生。多goroutine一起査詢cache發現沒有值,然一起調用f這個慢不拉的函。在得到結果後,也都去去更新map。其中一個獲得的結果會覆蓋掉另一個的結果。
理想情下是应该避免掉多的工作的。而这种“避免”工作一般被称为duplicate suppression(重复抑制/避免)。下面版本的Memo每一map元素都是指向一个条目的指。每一个条目包含对函数f调用结果的内容缓存。之前不同的是次entry包含了一叫ready的channel。在目的果被置之后,这个channel就会被关闭以向其它goroutine广播(§8.9)去读取该条目内的结果是安全的了。
理想情下是應該避免掉多的工作的。而這種“避免”工作一般被稱爲duplicate suppression(重複抑製/避免)。下面版本的Memo每一map元素都是指向一個條目的指。每一個條目包含對函數f調用結果的內容緩存。之前不同的是次entry包含了一叫ready的channel。在目的果被置之後,這個channel就會被關閉以向其它goroutine播(§8.9)去讀取該條目內的結果是安全的了。
```go
gopl.io/ch9/memo4
@@ -245,17 +245,17 @@ func (memo *Memo) Get(key string) (value interface{}, err error) {
}
```
在Get函包括下面些步了:取互斥锁来保护共享量cache map查询map中是否存在指定目,如果有找到那分配空插入一个新条目,放互斥。如果存在目的且其值没有写入完成(也就是有其它的goroutine在用f这个慢函数)时goroutine必等待值ready之才能读到条目的果。而想知道是否ready的,可以直接ready channel中取,由于这个读取操作在channel关闭之前一直是阻塞。
在Get函包括下面些步了:取互斥鎖來保護共享量cache map査詢map中是否存在指定目,如果有找到那分配空插入一個新條目,放互斥。如果存在目的且其值沒有寫入完成(也就是有其它的goroutine在調用f這個慢函數)時goroutine必等待值ready之才能讀到條目的果。而想知道是否ready的,可以直接ready channel中取,由於這個讀取操作在channel關閉之前一直是阻塞。
如果没有条目的需要向map中插入一个没有ready的目,前正在用的goroutine就需要负责调用慢函、更新目以及向其它所有goroutine广播条目已ready可的消息了。
如果沒有條目的需要向map中插入一個沒有ready的目,前正在調用的goroutine就需要負責調用慢函、更新目以及向其它所有goroutine廣播條目已ready可的消息了。
目中的e.res.value和e.res.err量是在多goroutine之共享的。创建条目的goroutine同时也会设置条目的值其它goroutine在收到"ready"的广播消息之立刻会去读取条目的值。尽管会被多goroutine同时访问,但却并不需要互斥。ready channel的关闭一定会发生在其它goroutine接收到广播事件之前,因此第一goroutine对这些变量的操作是一定生在这些读操作之前的。不会发生数据竞争
目中的e.res.value和e.res.err量是在多goroutine之共享的。創建條目的goroutine同時也會設置條目的值其它goroutine在收到"ready"的播消息之立刻會去讀取條目的值。盡管會被多goroutine同時訪問,但卻併不需要互斥。ready channel的關閉一定會發生在其它goroutine接收到播事件之前,因此第一goroutine對這些變量的操作是一定生在這些讀操作之前的。不會發生數據競爭
这样并发、不重复、无阻塞的cache就完成了。
這樣併發、不重複、無阻塞的cache就完成了。
上面这样Memo的实现使用了一互斥量来保护多个goroutine用Get的共享map量。不妨把这种设计和前面提到的把map量限在一个单独的monitor goroutine的方案做一些比,者在用Get需要消息。
上面這樣Memo的實現使用了一互斥量來保護多個goroutine調用Get的共享map量。不妨把這種設計和前面提到的把map量限在一個單獨的monitor goroutine的方案做一些比,者在調用Get需要消息。
Func、result和entry的明和之前保持一致:
Func、result和entry的明和之前保持一致:
```go
// Func is the type of the function to memoize.
@@ -273,7 +273,7 @@ type entry struct {
}
```
然而Memo类型现在包含了一叫做requests的channelGet的用方用这个channel和monitor goroutine通信。requests channel中的元素型是request。Get的用方会把这个结构中的两组key都填充好实际上用这两个变量来对函数进行缓存的。另一叫response的channel被拿来发送响应结果。这个channel只会传回一个单独的值。
然而Memo類型現在包含了一叫做requests的channelGet的調用方用這個channel和monitor goroutine通信。requests channel中的元素型是request。Get的調用方會把這個結構中的兩組key都填充好實際上用這兩個變量來對函數進行緩存的。另一叫response的channel被拿來發送響應結果。這個channel隻會傳迴一個單獨的值。
```go
gopl.io/ch9/memo5
@@ -301,9 +301,9 @@ func (memo *Memo) Get(key string) (interface{}, error) {
func (memo *Memo) Close() { close(memo.requests) }
```
上面的Get方法会创建一response channel把它放request结构中,然后发送给monitor goroutine后马上又接收到它。
上面的Get方法會創建一response channel把它放request結構中,然後發送給monitor goroutine後馬上又接收到它。
cache量被限在了monitor goroutine (\*Memo).server中下面看到。monitor在循中一直读取请直到request channel被Close方法关闭。每一个请求都会去查询cache如果有找到目的,那么就会创建/插入一新的目。
cache量被限在了monitor goroutine (\*Memo).server中下面看到。monitor在循中一直讀取請直到request channel被Close方法關閉。每一個請求都會去査詢cache如果有找到目的,那麽就會創建/插入一新的目。
```go
func (memo *Memo) server(f Func) {
@@ -335,13 +335,13 @@ func (e *entry) deliver(response chan<- result) {
}
```
和基互斥量的版本似,第一个对某个key的求需要负责去调用函数f并传入这个key将结果存在目里,并关闭ready channel来广播条目的ready消息。使用(\*entry).call完成上述工作。
和基互斥量的版本似,第一個對某個key的求需要負責去調用函數f併傳入這個key將結果存在目里,併關閉ready channel來廣播條目的ready消息。使用(\*entry).call完成上述工作。
接着同一key的请求会发现map中已有了存在的目,然后会等待结果变为ready并将结果从response发送给客户端的goroutien。上述工作是用(\*entry).deliver完成的。call和deliver方法的用必在自己的goroutine中行以保monitor goroutines不因此而被阻塞住而没法处理新的求。
接着同一key的請求會發現map中已有了存在的目,然後會等待結果變爲ready併將結果從response發送給客戶端的goroutien。上述工作是用(\*entry).deliver完成的。call和deliver方法的調用必在自己的goroutine中行以保monitor goroutines不因此而被阻塞住而沒法處理新的求。
这个例子明我们无论可以用上锁,还是通信建立并发程序都是可行的。
這個例子明我們無論可以用上鎖,還是通信建立併發程序都是可行的。
上面的两种方案不好特定情境下哪更好,不了解他们还是有值的。有时候从一种方式切到另一可以使你的代码更为简洁。(译注:不是好的golang推崇通信并发么)
上面的兩種方案不好特定情境下哪更好,不了解他們還是有值的。有時候從一種方式切到另一可以使你的代碼更爲簡潔。(譯註:不是好的golang推崇通信併發麽)
练习 9.3: 展Func型和(\*Memo).Get方法支持用方提供一个可选的done channel使其具备通过该channel取消整操作的能力(§8.9)。一被取消了的Func的调用结果不应该被缓存。
練習 9.3: 展Func型和(\*Memo).Get方法支持調用方提供一個可選的done channel使其具備通過該channel取消整操作的能力(§8.9)。一被取消了的Func的調用結果不應該被緩存。