C 多執行緒之旅實戰 自旋鎖那點事

2021-10-07 21:05:37 字數 4632 閱讀 7313

ticket lock

clh lock

mcs lock

總結前一篇文章講的是帶鎖的併發資料結構,而且講到了如果不帶鎖將會面臨什麼樣的問題。這一部分我將為大家帶來乙個全新的資料結構-自旋鎖。這是一種不使用阻塞庫的資料結構,我們將不使用阻塞庫的結構稱為非阻塞,但是不是所有的非阻塞資料結構都是無鎖的。

比如最常見的在多執行緒裡面的thread庫基本上都是阻塞函式。

自旋鎖(spinlock):是指當乙個執行緒在獲取鎖的時候,如果鎖已經被其它執行緒獲取,那麼該執行緒將迴圈等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出迴圈。

class

spinlock

void

lock()

void

unlock()

};spinlock sp;

void

fun_1()

void

fun_2()

intmain()

在這裡面使用atomic實現了互斥鎖的功能,雖然沒有呼叫任何阻塞函式但是依然不是無鎖資料結構。

所有的**圍繞著迴圈「旋轉」,這也是自旋鎖的由來。而且這樣也會像使用阻塞函式一樣,占用記憶體資源。

缺點如果某個執行緒持有鎖的時間過長,就會導致其它等待獲取鎖的執行緒進入迴圈等待,消耗cpu。使用不當會造成cpu使用率極高。

上面c++實現的自旋鎖不是公平的,即無法滿足等待時間最長的執行緒優先獲取鎖。不公平的鎖就會存在「執行緒飢餓」問題。

優點自旋鎖不會使執行緒狀態發生切換,一直處於使用者態,即執行緒一直都是active的;不會使執行緒進入阻塞狀態,減少了不必要的上下文切換,執行速度快。

非自旋鎖在獲取不到鎖的時候會進入阻塞狀態,從而進入核心態,當獲取到鎖的時候需要從核心態恢復,需要執行緒上下文切換。 (執行緒被阻塞後便進入核心(linux)排程狀態,這個會導致系統在使用者態與核心態之間來回切換,嚴重影響鎖的效能)。

這個例子在前一篇文章中有所體現,**見下,就是在喚醒執行緒時,由於時間原因導致最終喚醒失敗。

mutex mut;

condition_variable cond;

void

fun_3()

}void

fun_4()

}

適用場景

基於自旋鎖的各種優缺點,可以得出自旋鎖適合於對於鎖的競爭不激烈,且占用鎖時間非常短的**塊來說效能能大幅度的提公升,因為自旋的消耗會小於執行緒阻塞掛起再喚醒的操作的消耗,這些操作會導致執行緒發生兩次上下文切換!

如果鎖競爭激烈或者需要長時間占用鎖,這種情況就不適合自旋鎖,因為自旋鎖會一直占用cpu做無用功。

但是簡單的自旋鎖無法保證多執行緒競爭的公平性,即誰最先將flag設定為true誰就最先獲得鎖,這會導致執行緒飢餓!所以需要對自旋鎖進行改進,避免出現執行緒飢餓現象。

為了提供公平,有人發明了ticket lock

執行緒想要競爭某個鎖,需要先領一張ticket,然後監聽flag,發現flag被更新為手上的ticket的值了,才能去占領鎖

就像是在醫院看病一樣,醫生就是臨界區,病人就是執行緒,病人掛了號領一張單子,單子上寫了乙個獨一無二的號碼,病人等的時候就看螢幕,螢幕上顯示到自己的號碼了,才能進去找醫生。

atomicinteger ticket =

newatomicinteger(0

);volatile

int flag =0;

void

lock()

}void

unlock()

ticketlock 雖然解決了公平性的問題,但是多處理器系統上,每個程序/執行緒占用的處理器都在讀寫同乙個變數queuenum ,每次讀寫操作都必須在多個處理器快取之間進行快取同步,這會導致繁重的系統匯流排和記憶體的流量,大大降低系統整體的效能。

為了減少快取一致性帶來的開銷,clh lock被發明了。clh鎖的核心思想是,1. 競爭執行緒排隊 2. 監聽變數拆分

clh 是一種基於鍊錶的可擴充套件,高效能,公平的自旋鎖,申請執行緒只能在本地變數上自旋,它會不斷輪詢前驅的狀態,如果發現前驅釋放了鎖就結束自旋。

public

class

clhlock

// 尾部節點

private

volatile clhnode tail;

private

static final threadlocal local =

new threadlocal<

>()

;private

static final atomicreferencefieldupdater updater =

atomicreferencefieldupdater.

newupdater

(clhlock.

class

,clhnode.

class

,"tail");

public

void

lock()

prenode = null;

local.

set(node);}

// 如果不存在前驅節點,表示該鎖沒有被其他執行緒占用,則當前執行緒獲得鎖

}public

void

unlock()

node = null;

}}

初始時需要定義乙個dummy節點(dummpy.flag == true, dummy.prev == null),head == tail == dummy。當有執行緒想要獲取鎖時,先建立乙個鍊錶節點node,然後將node掛載在waitinglist的尾部(嘗試cas(tail, oldtail, node),如果成功將node.prev更新為oldtail,失敗則重試)。然後這個執行緒就監聽node.prev.flag,什麼時候node.prev.flag == false了,說明node的前乙個節點對應的執行緒已經釋放了鎖,本執行緒此時可以安全的占有鎖了。釋放鎖的時候,將對應的node.flag修改為false即可。

clh鎖並不是完美的,因為每個執行緒都是在前驅節點的locked欄位上自旋,而在numa體系中,有可能多個執行緒工作在多個不同的socket上的core裡。如果前驅節點的記憶體跟監聽執行緒的core距離過遠,會有效能問題。

mcs spinlock 是一種基於鍊錶的可擴充套件、高效能、公平的自旋鎖,申請執行緒只在本地變數上自旋,直接前驅負責通知其結束自旋,從而極大地減少了不必要的處理器快取同步的次數,降低了匯流排和記憶體的開銷。

mcs與clh最大的不同在於:clh是在前驅節點的locked域上自旋,mcs是在自己節點上的locked域上自旋。

具體的實現是,前驅節點在釋放鎖之後,會主動將後繼節點的locked域更新。

也就是把多次對遠端記憶體的監聽 + 一次對本地記憶體的更新,簡化成了多次對本地記憶體的監聽 + 一次對遠端記憶體的更新。

public

class

mcslock

public node lock()

if(null == oldtail)

oldtail.

setnext

(node)

;while

(node.

islocked()

)return node;

}public

void

unlock

(node node)

while

(node.

getnext()

!= null)

} node.

getnext()

.setlocked

(false);

}static

class

node

volatile boolean locked;

node next;

//後繼節點

public boolean islocked()

public

void

setlocked

(boolean locked)

public node getnext()

public

void

setnext

(node next)

}

自旋鎖:執行緒獲取鎖的時候,如果鎖被其他執行緒持有,則當前執行緒將迴圈等待,直到獲取到鎖。

自旋鎖等待期間,執行緒的狀態不會改變,執行緒一直是使用者態並且是活動的(active)。

自旋鎖如果持有鎖的時間太長,則會導致其它等待獲取鎖的執行緒耗盡cpu。

自旋鎖本身無法保證公平性,同時也無法保證可重入性。

那麼為了保證公平性又引出了ticketlock ,ticketlock 是採用排隊叫號的機制來實現的一種公平鎖,但是它每次讀寫操作都必須在多個處理器快取之間進行快取同步,這會導致繁重的系統匯流排和記憶體的流量,大大降低系統整體的效能。

所以我們又引出了clhlock和mcslock,clhlock和mcslock通過鍊錶的方式避免了減少了處理器快取同步,極大的提高了效能,區別在於clhlock是通過輪詢其前驅節點的狀態,而mcs則是檢視當前節點的鎖狀態。

linux多執行緒之自旋鎖

基本概念 何謂自旋鎖?它是為實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有乙個保持者,也就說,在任何時刻最多只能有乙個執行單元獲得鎖。但是兩者在排程機制上略有不同。對於互斥鎖,如果資源已經被占用,...

C 多執行緒之旅 3

閱讀目錄 一 介紹 二 通過tpl進入執行緒池 三 不用tpl進入到執行緒池 v部落格前言 先交代下背景,寫 c 多執行緒之旅 這個系列文章主要是因為以下幾個原因 1.多執行緒在c s和b s架構中用得是非常多的 2.而且多執行緒的使用是非常複雜的,如果沒有用好,容易造成很多問題。v寫在前面 多執行...

多執行緒的那點兒事(之讀寫鎖)

在編寫多執行緒的時候,有一種情況是十分常見的。那就是,有些公共資料修改的機會比較少。相比較改寫,它們讀的機會反而高的多。通常而言,在讀的過程中,往往伴隨著查詢的操作,中間耗時很長。給這種 段加鎖,會極大地降低我們程式的效率。那麼有沒有一種方法,可以專門處理這種多讀少寫的情況呢?有,那就是讀寫鎖。1 ...