串列的實現 利用go並存執行 添加互斥鎖 最終方法1使用指標標記 最終方法2使用用戶端伺服器模型
文中9.7節 Example:Concurrent Non-Blocking Cache
該例子實現一個功能,對函數進行緩衝,這樣函數對同樣的參數只需要計算一次。該方法還是concurrent-safe的,並且避免了對整個緩衝加鎖引起的競爭。
我們先來看串列的實現 串列的實現
func httpGetBody(url string) (interface{}, error) { resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body)}type result struct { value interface{} err error}type Func func(key string) (interface{}, error)type Memo struct { f Func cache map[string]result}func New(f Func) *Memo { return &Memo{f: f, cache: make(map[string]result)}}func (memo *Memo) Get(key string) (interface{}, error) { res, ok := memo.cache[key] if !ok { res.value, res.err = memo.f(key) memo.cache[key] = res } return res.value, res.err}func testCache() { incomingURLS := []string{"http://cn.bing.com/", "http://www.baidu.com", "http://cn.bing.com/", "http://www.baidu.com", "http://www.baidu.com", "http://cn.bing.com/", "http://www.baidu.com", "http://www.baidu.com", "http://cn.bing.com/", "http://www.baidu.com", "http://www.baidu.com", "http://cn.bing.com/", "http://www.baidu.com", "http://www.baidu.com", "http://cn.bing.com/", "http://www.baidu.com"} m := New(httpGetBody) allstart := time.Now() for _, url := range incomingURLS { start := time.Now() value, err := m.Get(url) if err != nil { fmt.Println(err) } fmt.Printf("%s, %s, %d bytes\n", url, time.Since(start), len(value.([]byte))) } fmt.Printf("all %s\n", time.Since(allstart))}
執行結果
http://cn.bing.com/, 180.576553ms, 120050 byteshttp://www.baidu.com, 25.863523ms, 99882 byteshttp://cn.bing.com/, 397ns, 120050 byteshttp://www.baidu.com, 245ns, 99882 byteshttp://www.baidu.com, 154ns, 99882 byteshttp://cn.bing.com/, 123ns, 120050 byteshttp://www.baidu.com, 136ns, 99882 byteshttp://www.baidu.com, 123ns, 99882 byteshttp://cn.bing.com/, 127ns, 120050 byteshttp://www.baidu.com, 188ns, 99882 byteshttp://www.baidu.com, 116ns, 99882 byteshttp://cn.bing.com/, 123ns, 120050 byteshttp://www.baidu.com, 118ns, 99882 byteshttp://www.baidu.com, 180ns, 99882 byteshttp://cn.bing.com/, 140ns, 120050 byteshttp://www.baidu.com, 124ns, 99882 bytesall 206.583298ms
利用go並存執行
我們利用sync.WaitGroup來等待所有URL解析完成
func testCache() { incomingURLS := []string{"http://cn.bing.com/", "http://www.baidu.com", "http://cn.bing.com/", "http://www.baidu.com", "http://www.baidu.com", "http://cn.bing.com/", "http://www.baidu.com", "http://www.baidu.com", "http://cn.bing.com/", "http://www.baidu.com", "http://www.baidu.com", "http://cn.bing.com/", "http://www.baidu.com", "http://www.baidu.com", "http://cn.bing.com/", "http://www.baidu.com"} m := New(httpGetBody) allstart := time.Now() var n sync.WaitGroup for _, url := range incomingURLS { start := time.Now() n.Add(1) go func(url string) { value, err := m.Get(url) if err != nil { fmt.Println(err) } fmt.Printf("%s, %s, %d bytes\n", url, time.Since(start), len(value.([]byte))) n.Done() }(url) n.Wait() } fmt.Printf("all %s\n", time.Since(allstart))}
結果可以看到時間更短了,但是裡面出現競爭關係了。
if !ok { res.value, res.err = memo.f(key) memo.cache[key] = res }
可能一個goroutine判斷!ok時,另外的goroutine也判斷!ok,f還是執行了多次。 添加互斥鎖
type Memo struct { f Func mu sync.Mutex cache map[string]result}func (memo *Memo) Get(key string) (interface{}, error) { memo.mu.Lock() defer memo.mu.Unlock() res, ok := memo.cache[key] if !ok { res.value, res.err = memo.f(key) memo.cache[key] = res } return res.value, res.err}
但是這樣造成一個問題,把Memo重新變回了串列訪問。 最終方法1:使用指標標記
作者的思路就是實現這樣的一個結果,一個goroutine調用函數,完成耗時的工作,其他調用同樣函數的goroutine等待函數執行完畢後立馬擷取結果。
type result struct { value interface{} err error}type entry struct { res result ready chan struct{}}type Func func(key string) (interface{}, error)type Memo struct { f Func mu sync.Mutex cache map[string]*entry}func New(f Func) *Memo { return &Memo{f: f, cache: make(map[string]*entry)}}func (memo *Memo) Get(key string) (interface{}, error) { memo.mu.Lock() e := memo.cache[key] if e == nil { e = &entry{ready: make(chan struct{})} memo.cache[key] = e memo.mu.Unlock() e.res.value, e.res.err = memo.f(key) close(e.ready) } else { memo.mu.Unlock() <-e.ready } return e.res.value, e.res.err}
Memo的cache成員由map[string]result變為map[string]*entry
entry的結構為:
type entry struct { res result ready chan struct{}}
ready通道用來通知其他goroutine函數執行完畢可以讀取結果了。
代碼的核心在Get函數中的下面部分
memo.mu.Lock() e := memo.cache[key] if e == nil { e = &entry{ready: make(chan struct{})} memo.cache[key] = e memo.mu.Unlock()
將函數f的計算從鎖地區中分離開了,通過memo.cache[key] = e實現只有一個goroutine會執行函數運算。 最終方法2:使用用戶端伺服器模型
專門一個服務進程負責緩衝,其它goroutine向該服務進程請求函數結果。
// Func is the type of the function to memoize. type Func func(key string) (interface{}, error) // A result is the result of calling a Func. type result struct { value interface{} err error } type entry struct { res result ready chan struct{} // closed when res is ready }
下面是關鍵區段代碼
type request struct { key string response chan<- result}type Memo struct { requests chan request}
Memo的成員是一個實值型別為request的通道requests,用來向伺服器發送函數請求,request類型包含一個result的通道,傳遞給伺服器後,伺服器用來給相應的goroutine傳遞函數的結果。
New函數主要建立了request通道和啟動伺服器:
func New(f Func) *Memo { memo := &Memo{request: make(chan request)} go memo.server(f) return memo}
Get函數建立result通道response,構建resquest,然後通過requests通道發送給伺服器,通過response接受函數結果。
func (memo *Memo) Get(key string) (interface{}, error) { response := make(chan result) memo.requests <- request{key, response} res := <-response return res.value, res.err}func (memo *Memo) Close() { close(memo.requests)}
接下來是伺服器程式
func (memo *Memo) server(f Func) { cache := make(map[string]*entry) for req := range memo.requests { e := cache[req.key] if e == nil { // This is the first request for this key. e = &entry{ready: make(chan struct{})} cache[req.key] = e go e.call(f, req.key) // call f(key) } go e.deliver(req.response) }}func (e *entry) call(f Func, key string) { // Evaluate the function. e.res.value, e.res.err = f(key) // Broadcast the ready condition. close(e.ready)}func (e *entry) deliver(response chan<- result) { //等待函數執行結束 <-e.ready //發送結果到用戶端 response <- e.res}