淺談go語言中的讀寫鎖和互斥鎖

Hello,各位小夥伴你們好,我是小棧君,近期氣溫有所降低,但願各位小夥伴記得防寒保暖,不要感冒了哦。數據庫

本期分享主題是關於go語言中的鎖的應用場景,以及爲各位小夥伴介紹實戰應用中最爲普遍的讀寫鎖和互斥鎖。編程

互聯網生態的日益繁榮,人們的生活便利獲得了極大的提升,經過網上操做咱們基本上能夠實現不少需求。安全

網站瘋狂訪問的背後應對的是一波接一波的挑戰。因此在應對系統的穩定和併發的時候,程序中的「鎖」就孕育而生。併發

互斥鎖

在編程中,引入了對象互斥鎖的概念,來保證共享數據操做的完整性。每一個對象都對應於一個可稱爲" 互斥鎖" 的標記,這個標記用來保證在任一時刻,只能有一個線程訪問該對象。高併發

也就是將共享資源變成獨佔資源。互斥鎖的應用場景一般是寫大於讀操做的,它不一樣於讀寫鎖的讀者隨意訪問,而寫者只有一個。工具

它表明的資源就是一個,無論是讀者仍是寫者,只要誰擁有了它,那麼其餘人就只有等待解鎖後,隱約在腦海中浮現出「寶刀屠龍,誰與針鋒」的話語。性能

其實咱們能夠很形象的理解一下互斥鎖,資源就比如是一個廁所,不少人都想上廁所,可是坑位只有一個,那麼誰獲取了互斥鎖,那麼誰就有權利進去,其餘人只有在門口排隊等待。也就是咱們一般所說的阻塞。區塊鏈

file

在go語言的sync包中也是有對於互斥鎖的解釋,互斥鎖的結構體很簡單,而且他的接口就只有一個加鎖和一個解鎖操做。測試

當value爲空時就是一個解鎖的互斥鎖,也就是其餘人均可以來使用。而且當互斥鎖第一次使用的時候就不能再被複制。大數據

file

而且在代碼中也有很詳細的說明,有興趣的小夥伴能夠參考代碼源碼進行了解。大體的意思就是說互斥鎖有兩種模式,正常和飢餓模式。

在正常的模式下采用的是FIFO模式即先進先出,可是會被等待者喚醒。沒有擁有互斥鎖的等待者會同新來的協程進行競爭,獲取鎖的使用權。

可是新來的協程擁有一個優點就是他們是運行在CPU上的,而以前有可能會有不少進程或協程須要被喚醒,因此他們有可能在毫秒之間就被人插隊了。

也就是新來的不須要被喚醒直接獲取到。若是一個在1ms的時間內沒有獲取到互斥量,那麼它將進入到飢餓模式。

也就是說在互斥鎖的飢餓模式下他會進行有序的交接。也就是會將互斥鎖的全部權進行移交到排在前面的等待者。

而新來的等待者想要獲取互斥鎖就只有乖乖排隊。他也不會試圖去搶佔互斥鎖。若是說在飢餓模式下他是最後一個互斥鎖的擁有者的話,或是等待少於1ms獲取鎖,那麼他就會從新轉變爲正常模式。

其實在正常模式下協程會有更好的性能,可是飢餓模式是爲了預防更多不肯定的狀況。

互斥鎖實戰:

咱們先進行一個簡單的模擬,定義個Map進行模擬數據庫對象,制定一個Map的切片來作爲數據對象的id和name,而且進行數據的初始化,當咱們開啓10個併發請求進行修改某一個值的時候。

file

咱們能夠看到最終的結果是修改爲功了。可是各位小夥伴咱們是真的就沒有任何問題了麼?

在go語言中咱們其實能夠檢查是否有問題可使用 go build -race 來進行數據競爭檢測。

[小知識:之後小夥伴在不肯定的狀況下均可以進行使用命令進行驗證,當咱們不肯定打包工具備哪些命令的時候,咱們能夠用go help build 來進行查看]

file

雙擊後咱們能夠看到出現瞭如下問題,看來當初咱們直接使用goland進行運行看到的表現每每有一絲絲的不穩當,仍是太年輕了啊!

file

咱們也能夠看到在點擊文件後獲得的結論是確實對於共享資源的搶佔是會致使問題的,並且在go語言中咱們也是很友好的進行了提早的測試,避免了在線上問題排查。

對於線程相關的問題在生產上其實排查相對而言是有點困難的。在文件中也能夠明確的顯示出問題出如今17行,也就是咱們在對於Map切片進行操做的時候。

file

因此咱們針對多併發訪問的時候須要對共享資源進行加鎖處理,目前模擬的應用場景是寫大於讀的時候。咱們使用互斥鎖進行相應的操做。

獲得的結論和以前的同樣,可是咱們一樣須要對這段程序進行go build -race 操做。

file

最終獲得了正確無誤的操做。以上就是咱們關於互斥鎖的初步使用。按照使用規範來說,無論是互斥鎖和讀寫鎖,咱們都應該儘量的對於小範圍的進行使用,在關鍵處進行使用,避免程序擁有大量的阻塞。

讀寫鎖

讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分紅讀者和寫者,讀者只對共享資源進行讀訪問,寫者則須要對共享資源進行寫操做。

這種鎖相對於自旋鎖而言,能提升併發性,由於在多處理器系統中,它容許同時有多個讀者來訪問共享資源,最大可能的讀者數爲實際的邏輯CPU數。

寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者(與CPU數相關),但不能同時既有讀者又有寫者。在讀寫鎖保持期間也是搶佔失效的。

若是讀寫鎖當前沒有讀者,也沒有寫者,那麼寫者能夠馬上得到讀寫鎖,不然它必須自旋在那裏,直到沒有任何寫者或讀者。

若是讀寫鎖沒有寫者,那麼讀者能夠當即得到該讀寫鎖,不然讀者必須自旋在那裏,直到寫者釋放該讀寫鎖。

因此針對於系統中須要讀多寫少的狀況下,咱們就須要使用到讀寫鎖進行應對程序的併發,保護程序的安全、穩定、高效的運行。

在go語言中內置包中已經實現了關於讀寫鎖操做,咱們在系統中須要使用能夠很方便的進行操做。

file

在代碼註釋中很明確的能夠看出,在go語言中的sync包中的RWMutex就是一個讀寫互斥鎖,該鎖能夠有任意數量的讀者或是隻有一個寫者持有,形象的說就比如一場電影能夠有無數的人來看,可是導演就只有一個。

當值爲空的時候,他是處於解鎖狀態,而且在讀寫鎖第一次使用的時候是不容許被拷貝的。若是一個協程(goroutine)持有讀的權限,另外一個協程進行鎖操做,那麼沒有任何一個協程可以再持有讀鎖,除非被釋放。

固然開發者也提醒了禁止進行遞歸讀鎖定,是爲了保證鎖可以一直可用。大概意思就是說這部電影不能讓你一我的獨自看,畢竟獨樂樂不如衆樂樂嘛。

在代碼中咱們經常使用的兩個操做就是Lock 和Unlock 即加解鎖操做,經過代碼中咱們能夠知道go語言最多讀者能夠高達10億個,已經可以完美的知足到咱們的業務需求了。

file

讀寫鎖實戰:

接下來咱們將模擬一下關於網頁中的讀寫鎖操做,讀寫鎖的應用場景多數是相似於朋友圈或微博的併發場景下的讀大於寫的場景,因此咱們用讀寫兩個循環協程進行模擬數據庫的請求操做,並是使用count進行原子計數,最終獲得的結果以下:

file

固然若是咱們依舊關閉鎖操做會獲得相同的結果麼?

file

使用goland執行後的結果和以前加鎖的狀態是是沒有什麼區別,可是真的沒有什麼區別麼?

讓咱們使用一下go build -race 命令,最後獲得的結果以下:

file

表面風輕雲淡的狀況下內部已經翻江倒海了,因此小棧君在這裏也是奉勸各位三思然後行。

讀寫鎖與互斥鎖性能大比拼

讀寫鎖和互斥鎖的各自實戰狀況已經初略的給各位分享了一遍,整體而言用法是比較簡單的,而且有興趣的小夥伴能夠看看go語言的內置包,後續我也會陸續爲你們分享關於go語言實現二叉樹,鏈表結構等文章,讓你們更加深刻的感覺到go語言的魅力。

固然我也在籌劃其餘語言的分享,因此各位小夥伴,若是你喜歡個人文章,麻煩分享並關注小棧君哦。

迴歸正題,咱們在使用各自的使用場景下並無感覺到讀寫鎖魚互斥鎖性能上有多大的區別。因此小棧君接下來就一個場景分別使用兩個鎖來進行模擬請求計數,得出結論。

咱們首先使用讀寫鎖進行模擬如圖所示:

file

咱們先定義讀寫鎖、數據庫數據、還有相關的原子計數器。這裏有一個小知識點,咱們在計數的時候用了atomic,這樣能夠保證計數的準確和安全。而後開啓了一個寫協程和一百個讀協程。

爲了加大請求力度咱們有在每一個協程裏開啓了無線循環讀取,用1毫秒模擬查詢時間,用10毫秒模擬寫時間。最終得出的結論是計數器計算到了16萬接近17萬次。

file

而後相同的代碼咱們僅僅只修改了一下鎖的機制,將讀寫鎖改爲了互斥鎖,各位能夠看到效果就是代碼執行了1695次,相差是很是大的。

file

file

以上就是關於go語言中的互斥鎖和讀寫鎖的分享。事實證實,只有鎖用的對,咱們就能夠早些下班啦。

好了,今天的分享就到這啦,若是你喜歡個人分享,麻煩你點擊一個好看或贊,我是小棧君,不按期分享IT乾貨,包括但不限於區塊鏈、大數據、Python、go、等系列專題。原創不易,更新較慢,多多包涵。但願與你共同成長。咱們下期再見啦,拜了個拜~

相關文章
相關標籤/搜索