這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。在go語言裡,提倡用通道通訊的方式來替代顯式的同步機制。但是我發現有的時候用通道通訊方式實現的似乎也不是很好(暫不考慮效率問題)。假設有一個帳號的集合,需要在這個集合上實現一些操作,比如尋找修改等。這個集合的操作必須是支援並發的。
如果用鎖的方式(方案1)
實現大概是這樣:
import "sync"type Info struct {age int}type AccountMap struct {accounts map[string]*Infomutex sync.Mutex}func NewAccountMap() *AccountMap {return &AccountMap{accounts: make(map[string]*Info),}}func (p *AccountMap) add(name string, age int) {p.mutex.Lock()defer p.mutex.Unlock()p.accounts[name] = &Info{age}}func (p *AccountMap) del(name string) {p.mutex.Lock()defer p.mutex.Unlock()delete(p.accounts, name)}func (p *AccountMap) find(name string) *Info {p.mutex.Lock()defer p.mutex.Unlock()res, ok := p.accounts[name]if !ok {return nil}inf := *resreturn &inf}
用通道來實現試試(方案2)
type Info struct {age int}type AccountMap struct {accounts map[string]*Infoch chan func()}func NewAccountMap() *AccountMap {p := &AccountMap{accounts: make(map[string]*Info),ch: make(chan func()),}go func() {for {(<-p.ch)()}}()return p}func (p *AccountMap) add(name string, age int) {p.ch <- func() {p.accounts[name] = &Info{age}}}func (p *AccountMap) del(name string) {p.ch <- func() {delete(p.accounts, name)}}func (p *AccountMap) find(name string) *Info {// 每次查詢都要建立一個通道c := make(chan *Info)p.ch <- func() {res, ok := p.accounts[name]if !ok {c <- nil} else {inf := *resc <- &inf}}return <-c}
這裡有個問題,每次調用find都要建立一個通道。
那麼試試把通道作為參數(方案3)
只需要修改find函數的實現:
// 通道對象作為參數,暴露了實現機制func (p *AccountMap) find(name string, c chan *Info) *Info {p.ch <- func() {res, ok := p.accounts[name]if !ok {c <- nil} else {inf := *resc <- &inf}}return <-c}
總結一下,現在的問題就是三種方案都有不盡如人意之處:
方案1:使用鎖機制,不太符合go解決問題的方式。
方案2:對於需要返回結果的查詢,每次查詢都要建立一個通道,比較浪費資源。
方案3:需要在函數參數中指定通道對象,把實現機制暴露了。
那麼有沒有什麼更好的方案呢?
2012.12.14:方案2 還有一個改進版本:利用預分配以及可回收的channel來提高資源使用率。這個技術在多個goroutine等待一個主動對象返回自己的資料時會比較有用。例如網遊伺服器中登入伺服器裡每個玩家的串連用一個goroutine來處理;另外一個主動對象代表帳號伺服器串連用於驗證帳號合法性。玩家goroutine會把各自的輸入的玩家帳號密碼發送給這個主動對象,並阻塞等待主動對象返回驗證結果。因為有多個玩家同時發起帳號驗證請求,所以主動對象需要把返回結果進行分發,因此可以在發送請求的時候申請一個通道並等待這個通道。
代碼如下:
type Info struct {age int}type AccountMap struct {accounts map[string]*Infoch chan func()tokens chan chan *Info}func NewAccountMap() *AccountMap {p := &AccountMap{accounts: make(map[string]*Info),ch: make(chan func()),tokens: make(chan chan *Info, 128),}for i := 0; i < cap(p.tokens); i++ {p.tokens <- make(chan *Info)}go func() {for {(<-p.ch)()}}()return p}func (p *AccountMap) add(name string, age int) {p.ch <- func() {p.accounts[name] = &Info{age}}}func (p *AccountMap) del(name string) {p.ch <- func() {delete(p.accounts, name)}}func (p *AccountMap) find(name string) *Info {// 每次查詢都要擷取一個通道c := <-p.tokensp.ch <- func() {res, ok := p.accounts[name]if !ok {c <- nil} else {inf := *resc <- &inf}}inf := <-c// 回收通道p.tokens <- creturn inf}
補充一下golang-china上的評論:
xushiwei
在你的方式裡面,用 channel 其實把所有請求序列化。另外,從成本上來說,channel 遠大於鎖。因為 channel 本身顯然是用鎖 + 訊號喚醒機制實現的。
steve wang
是不是可以這樣總結:1.對於共用給各個goroutine的資料對象的並發訪問,使用鎖來控制2.對於goroutine之間的通訊,使用通道
longshanksmo
單就效能來看,現在下這種結論有些草率。並發和效能問題錯宗複雜,不同的情境可能會產生完全相反的結論。還有眾多因素需要考慮:首先,不同的用況下,鎖粒度不同。在你的案例中是map操作,鎖粒度很小。但如果是某種重載操作,或者存在阻塞,鎖粒度會很大。那時用鎖就不划算。其次,chan的鎖粒度很小,基本固定,可預測。在實際業務中,效能可預測非常重要,決定了部署時的資源投入和調配。最重要一點,如果進程內的所有goroutine是在單個線程內運行,那麼chan的鎖是不需要的。這樣才能真正發揮coroutine的優勢。現在的go編譯器似乎還沒有對這個做最佳化,不知將來是否會進化。總之,並發方面還沒有一改而論