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的調用結果不應該被緩存。