1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        淺談 Golang 鎖的應(yīng)用: sync包

        共 2164字,需瀏覽 5分鐘

         ·

        2021-10-25 01:02


        Go 語言 sync 包中的鎖都在什么場(chǎng)景下用?怎么用?本文對(duì) sync 包內(nèi)的鎖做了梳理。

        今天談一下鎖,以及 Go 里面 Sync 包里面自帶的各種鎖,說到鎖這個(gè)概念,在日常生活中,鎖是為了保護(hù)一些東西,比如門鎖、密碼箱鎖,可以理解對(duì)資源的保護(hù)。在編程里面,鎖也是為了保護(hù)資源,比如說對(duì)文件加鎖,同一時(shí)間只也許一個(gè)用戶修改,這種鎖一般叫作文件鎖。

        實(shí)際開發(fā)中,鎖又可分為互斥鎖(排它鎖)、讀寫鎖、共享鎖、自旋鎖,甚至還有悲觀鎖、樂觀鎖這種說法。在 Mysql 數(shù)據(jù)庫里面鎖的應(yīng)用更多,比如行鎖、表鎖、間隙鎖,有點(diǎn)眼花繚亂。拋開這些概念,在編程領(lǐng)域,鎖的本質(zhì)是為了解決并發(fā)情況下對(duì)數(shù)據(jù)資源的訪問問題,如果我們不加鎖,并發(fā)讀寫一塊數(shù)據(jù)必然會(huì)產(chǎn)生問題,如果直接加個(gè)互斥鎖問題是解決了,但是會(huì)嚴(yán)重影響讀寫性能,所以后面又產(chǎn)生了更復(fù)雜的鎖機(jī)制,在數(shù)據(jù)安全性和性能之間找到最佳平衡點(diǎn)。

        正常來說,只有在并發(fā)編程下才會(huì)需要鎖,比如說多個(gè)線程(在 Go 里面則是協(xié)程)同時(shí)讀寫一個(gè)文件,下面我以一個(gè)文件為例,來解釋這幾種鎖的概念:

        如果我們使用互斥鎖,那么同一時(shí)間只能由一線程去操作(讀或?qū)懀?,這就是像是咱們?nèi)ド蠋?,一個(gè)坑位同一時(shí)間只能蹲一個(gè)人,這就是廁所門鎖的作用。

        如果我們使用讀寫鎖,意味著可以同時(shí)有多個(gè)線程讀取這個(gè)文件,但是寫的時(shí)候不能讀,并且只能由一個(gè)線程去寫。這個(gè)鎖實(shí)際上是互斥鎖的改進(jìn)版,很多時(shí)候我們之所以給文件加鎖是為了避免你在寫的過程中有人讀到了臟數(shù)據(jù)。

        如果我們使用共享鎖,根據(jù)我查到資料,這種叫法大多數(shù)是源自 MySQL 事務(wù)里面的鎖概念,它意味著只能讀數(shù)據(jù),并不能修改數(shù)據(jù)。

        如果我們使用自旋鎖,則意味著當(dāng)一個(gè)線程在獲取鎖的時(shí)候,如果鎖已經(jīng)被其它線程獲取,那么該線程將循環(huán)等待,然后不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會(huì)退出循環(huán)。

        這些鎖的機(jī)制在 Go 里面有什么應(yīng)用呢,下面大家一起看看 Go 標(biāo)準(zhǔn)庫里面 sync 包提供的一些非常強(qiáng)大的基于鎖的實(shí)現(xiàn)。

        1. 文件鎖

        文件鎖和 sync 包沒關(guān)系,這里面只是順便說一下,舉個(gè)例子,磁盤上面有一個(gè)文件,必須保證同一時(shí)間只能由一個(gè)人打開,這里的同一時(shí)間是指操作系統(tǒng)層面的,并不是指應(yīng)用層面,文件鎖依賴于操作系統(tǒng)實(shí)現(xiàn)。

        在 C 或 PHP 里面,文件鎖會(huì)使用一個(gè) flock 的函數(shù)去實(shí)現(xiàn),其實(shí) Go 里面也類似:

        func?main()?{
        ????var?f?=?"/var/logs/app.log"
        ????file,?err?:=?os.OpenFile(f,?os.O_RDWR,?os.ModeExclusive)
        ????if?err?!=?nil?{
        ????????panic(err)
        ????}
        ????defer?file.Close()

        ????//?調(diào)用系統(tǒng)調(diào)用加鎖
        ????err?=?syscall.Flock(int(file.Fd()),?syscall.LOCK_EX|syscall.LOCK_NB)
        ????if?err?!=?nil?{
        ????????panic(err)
        ????}
        ????defer?syscall.Flock(int(file.Fd()),?syscall.LOCK_UN)
        ????//?讀取文件內(nèi)容
        ????all,?err?:=?ioutil.ReadAll(file)
        ????if?err?!=?nil?{
        ????????panic(err)
        ????}

        ????fmt.Printf("%s",?all)
        ????time.Sleep(time.Second?*?10)?//模擬耗時(shí)操作
        }

        需要說明一下,F(xiàn)lock 函數(shù)第一個(gè)參數(shù)是文件描述符,第二個(gè)參數(shù)是鎖的類型,分為 LOCK_EX(排它鎖)、LOCK_SH(讀共享鎖)、LOCK_NB(遭遇鎖的表現(xiàn),遇到排它鎖的時(shí)候默認(rèn)會(huì)被阻塞,NB 即非阻塞,直接返回 Error)、LOCK_UN(解鎖)。

        如果這時(shí)候你打開另外一個(gè)終端再次運(yùn)行這個(gè)程序你會(huì)發(fā)現(xiàn)報(bào)錯(cuò)信息如下:

        panic:?resource?temporarily?unavailable

        文件鎖保證了一個(gè)文件在操作系統(tǒng)層面的數(shù)據(jù)讀寫安全,不過實(shí)際應(yīng)用中并不常見,畢竟大部分時(shí)候我們都是使用數(shù)據(jù)庫去做數(shù)據(jù)存儲(chǔ),極少使用文件。

        2.sync.Mutex

        下面我所說的這些鎖都是應(yīng)用級(jí)別的鎖,位于 Go 標(biāo)準(zhǔn)庫 sync 包里面,各有各的應(yīng)用場(chǎng)景。

        這是一個(gè)標(biāo)準(zhǔn)的互斥鎖,平時(shí)用的也比較多,用法也非常簡(jiǎn)單,lock 用于加鎖,unlock 用于解鎖,配合 defer 使用,完美。

        為了更好的展示鎖的應(yīng)用,這個(gè)舉一個(gè)沒有實(shí)際意義的例子,給一個(gè) int 變量做加法,用 2 個(gè)協(xié)程并發(fā)的去做加法。

        var?i?int

        func?main()?{
        ????go?add(&i)

        ????time.Sleep(time.Second?*?3)

        ????println(i)
        }

        func?add(i?*int)?{
        ????for?j?:=?0;?j?10000;?j++?{
        ????????*i?=?*i?+?1
        ????}
        }

        我們想要得到的正常結(jié)果是 20000,然而實(shí)際上并不是,其結(jié)果是不固定的,很可能少于 20000,大家多運(yùn)行幾次便可得知。

        假設(shè)你多加一行 runtime.GOMAXPROCS(1),你會(huì)發(fā)現(xiàn)結(jié)果一直是正確的,這是為什么呢?

        用一個(gè)比較理論的說法,這是因?yàn)楫a(chǎn)生了數(shù)據(jù)競(jìng)爭(zhēng)(data race)問題,在 Go 里面我們可以在 go run 后面加上?-race?來檢測(cè)數(shù)據(jù)競(jìng)爭(zhēng),結(jié)果會(huì)告訴你在哪一行產(chǎn)生的,非常實(shí)用。

        go?run?-race?main.go
        ==================
        WARNING:?DATA?RACE
        Read?at?0x00000056ccb8?by?goroutine?7:
        ??main.add()
        ??????main.go:23?+0x43
        Previous?write?at?0x00000056ccb8?by?goroutine?6:
        ??main.add()
        ???????main.go:23?+0x59
        Goroutine?7?(running)?created?at:
        ??main.main()
        ???????main.go:14?+0x76
        Goroutine?6?(running)?created?at:
        ??main.main()
        ???????main.go:13?+0x52
        ==================
        20000
        Found?1?data?race(s)
        exit?status?66

        解決這個(gè)問題,有多種解法,我們當(dāng)然可以換個(gè)寫法,比如說用 chan 管道去做加法(chan 底層也用了鎖),實(shí)際上在 Go 里面更推薦去使用 chan 解決數(shù)據(jù)同步問題,而不是直接用鎖機(jī)制。

        在上面的這個(gè)例子里面我們需要在 add 方法里面寫,每次操作之前 lock,然后 unlock:

        func?add(i?*int)?{
        ????for?j?:=?0;?j?10000;?j++?{
        ????????s.Lock()
        ????????*i?=?*i?+?1
        ????????s.Unlock()
        ????}
        }

        3.sync.RWMutex

        讀寫鎖是互斥鎖的升級(jí)版,它最大的優(yōu)點(diǎn)就是支持多讀,但是讀和寫、以及寫與寫之間還是互斥的,所以比較適合讀多寫少的場(chǎng)景。

        它的實(shí)現(xiàn)里面有 5 個(gè)方式:

        func?(rw?*RWMutex)?Lock()
        func?(rw?*RWMutex)?RLock()
        func?(rw?*RWMutex)?RLocker()?Locker
        func?(rw?*RWMutex)?RUnlock()
        func?(rw?*RWMutex)?Unlock()

        其中 Lock() 和 Unlock() 用于申請(qǐng)和釋放寫鎖,RLock() 和 RUnlock() 用于申請(qǐng)和釋放讀鎖,RLocker() 用于返回一個(gè)實(shí)現(xiàn)了 Lock() 和 Unlock() 方法的 Locker 接口。

        實(shí)話說,平時(shí)這個(gè)用的真不多,主要是使用起來比較復(fù)雜,雖然在讀性能上面比?Mutex?要好一點(diǎn)。

        4.sync.Map

        這個(gè)類型印象中是后來加的,最早很多人使用互斥鎖來并發(fā)的操作 map,現(xiàn)在也還有人這么寫:

        type?User?struct?{
        ????m?map[string]string
        ????l?sync.Mutex
        }

        也就是一個(gè) map 配一把鎖的寫法,可能是這種寫法比較多,于是乎官方就在標(biāo)準(zhǔn)庫里面實(shí)現(xiàn)了一個(gè)?sync.Map, 是一個(gè)自帶鎖的 map,使用起來方便很多,省心。

        var?m?sync.Map

        func?main()?{
        ????m.Store("1",?1)
        ????m.Store("2",?1)
        ????m.Store("3",?1)
        ????m.Store(4,?"5")?//?注意類型

        ????load,?ok?:=?m.Load("1")
        ????if?ok?{
        ????????fmt.Printf("%v\n",?load)
        ????}

        ????load,?ok?=?m.Load(4)
        ????if?ok?{
        ????????fmt.Printf("%v\n",?load)
        ????}
        }

        需要注意的一點(diǎn)是這個(gè) map 的 key 和 value 都是 interface{}類型,所以可以隨意放入任何類型的數(shù)據(jù),在使用的時(shí)候就需要做好斷言處理。

        5.sync.Once

        package?main

        import?"sync"

        var?once?sync.Once

        func?main()?{
        ????doOnce()
        }

        func?doOnce()?{
        ????once.Do(func()?{
        ????????println("one")
        ????})
        }

        執(zhí)行結(jié)果只打印了一個(gè) one,所以 sync.Once 的功能就是保證只執(zhí)行一次,也算是一種鎖,通常可以用于只能執(zhí)行一次的初始化操作,比如說單例模式里面的懶漢模式可以用到。

        6.sync.Cond

        這個(gè)一般稱之為條件鎖,就是當(dāng)滿足某些條件下才起作用的鎖,啥個(gè)意思呢?舉個(gè)例子,當(dāng)我們執(zhí)行某個(gè)操作需要先獲取鎖,但是這個(gè)鎖必須是由某個(gè)條件觸發(fā)的,其中包含三種方式:

        • 等待通知:wait, 阻塞當(dāng)前線程,直到收到該條件變量發(fā)來的通知
        • 單發(fā)通知:signal, 讓該條件變量向至少一個(gè)正在等待它的通知的線程發(fā)送通知,表示共享數(shù)據(jù)的狀態(tài)已經(jīng)改變
        • 廣播通知:broadcast, 讓條件變量給正在等待它的通知的所有線程都發(fā)送通知

        下面看一個(gè)簡(jiǎn)單的例子:

        package?main
        import?(
        ????"sync"
        ????"time"
        )

        var?cond?=?sync.NewCond(&sync.Mutex{})

        func?main()?{
        ????for?i?:=?0;?i?10;?i++?{
        ????????go?func(i?int)?{
        ????????????cond.L.Lock()
        ????????????cond.Wait()?//?等待通知,阻塞當(dāng)前?goroutine
        ????????????println(i)
        ????????????cond.L.Unlock()
        ????????}(i)
        ????}

        ????//?確保所有協(xié)程啟動(dòng)完畢
        ????time.Sleep(time.Second?*?1)

        ????cond.Signal()

        ????//?確保結(jié)果有時(shí)間輸出
        ????time.Sleep(time.Second?*?1)
        }

        開始我們使用 for 循環(huán)啟動(dòng) 10 個(gè)協(xié)程,每個(gè)協(xié)程都在等待鎖,然后使用 signal 發(fā)送一個(gè)通知。

        如果你多次運(yùn)行,你會(huì)發(fā)現(xiàn)打印的結(jié)果也是隨機(jī)從 0 到 9,說明各個(gè)協(xié)程之間是競(jìng)爭(zhēng)的,鎖是起到作用的。如果把 singal 替換成 broadcast,則會(huì)打印所有結(jié)果。

        講實(shí)話,我暫時(shí)也沒有發(fā)現(xiàn)有哪些應(yīng)用場(chǎng)景,感覺這個(gè)應(yīng)該適合需要非常精細(xì)的協(xié)程控制場(chǎng)景,大家先了解一下吧。

        7.sync.WaitGroup

        這個(gè)大多數(shù)人都用過,一般用來控制協(xié)程執(zhí)行順序,大家都知道如果我們直接用 go 啟動(dòng)一個(gè)協(xié)程,比如下面這個(gè)寫法:

        go?func()?{
        ????println("1")
        }()

        time.Sleep(time.Second?*?1)?//?睡眠?1s

        如果沒有后面的 sleep 操作,協(xié)程就得不到執(zhí)行,因?yàn)檎麄€(gè)函數(shù)結(jié)束了,主進(jìn)程都結(jié)束了協(xié)程哪有時(shí)間執(zhí)行,所以有時(shí)候?yàn)榱朔奖憧梢灾苯雍?jiǎn)單粗暴的睡眠幾秒,但是實(shí)際應(yīng)用中不可行。這時(shí)候就可以使用 waitGroup 解決這個(gè)問題,舉個(gè)例子:

        package?main

        import?"sync"

        var?wg?sync.WaitGroup

        func?main()?{
        ????for?i?:=?0;?i?10;?i++?{
        ????????wg.Add(1)?//?計(jì)數(shù)+1
        ????????go?func()?{
        ????????????println("1")
        ????????????wg.Done()?//?計(jì)數(shù)-1,相當(dāng)于?wg.add(-1)
        ????????}()
        ????}
        ????wg.Wait()?//?阻塞帶等待所有協(xié)程執(zhí)行完畢
        }

        8.sync.Pool

        這是一個(gè)池子,但是卻是一個(gè)不怎么可靠的池子,sync.Pool 初衷是用來保存和復(fù)用臨時(shí)對(duì)象,以減少內(nèi)存分配,降低 CG 壓力。

        說它不可靠是指放進(jìn) Pool 中的對(duì)象,會(huì)在說不準(zhǔn)什么時(shí)候被 GC 回收掉,所以如果事先 Put 進(jìn)去 100 個(gè)對(duì)象,下次 Get 的時(shí)候發(fā)現(xiàn) Pool 是空也是有可能的。

        package?main

        import?(
        ????"fmt"
        ????"sync"
        )

        type?User?struct?{
        ????name?string
        }

        var?pool?=?sync.Pool{
        ????New:?func()?interface{}?{
        ????????return?User{
        ????????????name:?"default?name",
        ????????}
        ????},
        }

        func?main()?{
        ????pool.Put(User{name:?"name1"})
        ????pool.Put(User{name:?"name2"})

        ????fmt.Printf("%v\n",?pool.Get())?//?{name1}
        ????fmt.Printf("%v\n",?pool.Get())?//?{name2}
        ????fmt.Printf("%v\n",?pool.Get())?//?{default?name}?池子已空,會(huì)返回?New?的結(jié)果
        }

        從輸出結(jié)果可以看到,Pool 就像是一個(gè)池子,我們放進(jìn)去什么東西,但不一定可以取出來(如果中間有 GC 的話就會(huì)被清空),如果池子空了,就會(huì)使用之前定義的 New 方法返回的結(jié)果。

        為什么這個(gè)池子會(huì)放到 sync 包里面呢?那是因?yàn)樗幸粋€(gè)重要的特性就是協(xié)程安全的,所以其底層自然也用到鎖機(jī)制。

        至于其應(yīng)用場(chǎng)景,知名的 Web 框架 Gin 里面就有用到,在處理用戶的每條請(qǐng)求時(shí)都會(huì)為當(dāng)前請(qǐng)求創(chuàng)建一個(gè)上下文環(huán)境 Context,用于存儲(chǔ)請(qǐng)求信息及相應(yīng)信息等。Context 滿足長(zhǎng)生命周期的特點(diǎn),且用戶請(qǐng)求也是屬于并發(fā)環(huán)境,所以對(duì)于線程安全的 Pool 非常適合用來維護(hù) Context 的臨時(shí)對(duì)象池。

        轉(zhuǎn)自:wangbjun.site/2020/coding/golang/locker.html

        文章轉(zhuǎn)載:Go開發(fā)大全

        (版權(quán)歸原作者所有,侵刪)


        點(diǎn)擊下方“閱讀原文”查看更多

        瀏覽 111
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            精品无码一区二区三区天香 | 中国产公妇仑在线观看 | 国产极品大陆一区二区伦理片 | 打开双腿扒开灌红酒play | 一级片中文字幕 | 色综合第一页 | 强奸一级毛片 | 韩国电影《少妇的激情》 | 日韩一区二区三区四区中文在线 | 性欧美长视频 |