Linux 多執行緒 」一寫多讀」 模式下的無鎖設計

2021-08-14 11:35:36 字數 3087 閱讀 7497

延伸結語

在linux多執行緒環境下對同一變數進行讀寫時,經常會遇到讀寫的原子性問題,即會出現競爭條件。為了解決多個執行緒對同一變數訪問時的競爭條件問題,作業系統層面提供了鎖、訊號量、條件變數等幾種執行緒同步機制。如果對變數的每次訪問都使用上述機制,由於系統呼叫會陷入核心空間,需要頻繁的進行上下文切換,這就導致了程式的時間開銷比較大。

自然的,我們就想到,在多執行緒環境中,在某些情況下是否能減少甚至避免使用系統呼叫?答案是肯定的。

如果對多執行緒下的變數訪問進行分析,可以看到,執行緒對變數的訪問可以分為以下幾類:

在linux 系統中,多個執行緒同時讀乙個變數是不需要同步的,而多個執行緒同時寫乙個變數或乙個執行緒寫而其他執行緒讀某個變數,是需要同步的,可以總結為:」多讀不互斥,而讀寫和多寫互斥「。

由於多個執行緒對同一變數的讀不需要同步,因而一寫多讀和一寫一讀並無本質區別,進而可以把多執行緒下對變數訪問依據是否需要同步而合併成如下三類:

解決上面所有的互斥,都可以使用系統呼叫。上面已經提到,在某些情況下我們是可以避免使用代價高昂的系統呼叫的。而「一寫多讀」就是這些特殊情況中的一種。

使用系統呼叫進行同步的主要問題在於頻繁切換上下文耗時較長,而後台系統的處理速度又是除正確性之外最為關鍵的指標。為提高系統的執行速度,我們可以使用用其他系統資源來換取時間的辦法,從而避免使用鎖之類系統呼叫。在這些方法中,最常見的就是用空間換取時間。

針對一寫多讀的情況,可以使用」雙 buffer「 及共享指標機制來實現對同一變數高效訪問,同時又能保證不會出現競爭條件。這一實現的技術關鍵點在於以下兩個方面:

注意ptr 和 bak_ptr 都是整個map 的指標,上面藍色箭頭表示通過兩個指標訪問 map 中的元素,ptr 和bak_ptr 本身並不指向元素。

在系統啟動時,把兩個智慧型指標分別初始化為乙個主map 和乙個備份 map。之後把全部資料更新到主map中開始對外提供服務。當外部需要讀取資料時(多讀),全部通過主map 的智慧型指標 ptr 來實現。而資料的更新全部通過備份map 的指標bak_ptr 來實現。由此可以看出,由於使用了兩個map,即雙buffer,使得資料的讀和寫進行了分離,互不影響,不會出現競爭條件,避免了鎖的使用。

由於讀寫分離,雙buffer機制下的資料讀寫不會出現競爭條件。在備份map 中資料更新完成時,必然需要一種方式,使得新資料能被使用到。這裡需要做的就是把主map和備份map 的共享指標指向的內容互換,即ptr 和bak_ptr 指向的內容互換。指標切換如下圖所示:

那麼,在指標互換時,會出現什麼問題呢?

在指標的切換過程中,會出現如下兩個問題:

當指標切換時,單執行緒對 bak_ptr 的寫操作已經完成,因而對其可以隨便讀寫。但由於多個讀執行緒可能還在使用ptr,切換指標時對 ptr 的讀寫就要十分的小心。為了避免對 ptr 的讀寫出現競爭條件,本文使用了自旋鎖來對ptr 的讀寫進行同步。使用自旋鎖的原因有兩個:

上面已經介紹了指標訪問丟失的情形,即在兩個指標切換時,多個讀執行緒可能正在使用ptr。為了避免出現讀執行緒會讀取到無效資料,本文使用的方法是利用共享指標的引用計數來實現指標的延遲互換。

解決ptr 的競爭條件和指標訪問丟失問題後,就可以安全的使用雙buffer 方案了。

最終的**如下,其中 map_ptr_ 就是主map 指標,bak_ptr_ 是備份map 的指標:

class

updatedata

void

periodtask();

void

setflag(int

i) private

: shared_ptr

> map_ptr_;

spinlock map_rwspinlock_;

shared_ptr

> bak_map_ptr_;

intflag_;

shared_ptr

void

> new_map_ptr);

void

void

periodtask();

void

getdata(shared_ptr

> ptr)

};// 獲取主map 指標

shared_ptr

lock(map_rwspinlock_); // 加自旋鎖,避免對 ptr 訪問出現競爭條件

return

map_ptr_; // 主map 指標

}// 設定主map 指標

void

> new_map_ptr)

// 真正的切換指標

void

shared_ptr

// 用引用次數來解決訪問丟失問題

while

(old_map_ptr.unique()

bak_map_ptr_ = old_map_ptr;

bak_map_ptr_->clear();

}// 定時任務

void

updatedata::periodtask()

}

從上面可以看出,通過使用雙buffer和共享指標,避免了在一寫多讀模式中對資料的讀寫頻繁加鎖,實現了」無鎖「 的設計。

即然雙buffer可以很好的用於一寫多讀模式,那麼對於」多寫一讀「或」多寫多讀「模式,是否也可以引入雙buffer 模式呢?

在含有多執行緒寫同一變數的情形下下,其實是不太適合使用雙buffer 方案的。主要原因是:

因而,在多寫的模式下,還是優先用讀寫鎖等作業系統提供的同步機制。

雙buffer 方案在多執行緒環境下能較好的解決 「一寫多讀」 時的資料更新問題,特別是適用於資料需要定期更新,且一次更新資料量較大的情形。而這種情形在後台開發中十分常見。

對技術的不斷探索與思考往往會有新的收穫,我們誤闖入的世界,也往往能帶給我們不一樣的精彩。

我思故我在。

我就是我,疾馳中的企鵝。

我就是我,不一樣的焰火。

一讀一寫情況下,無鎖環形佇列如何實現?

無鎖環形佇列的設計及示例,讀寫佇列最大的應用是 乙個執行緒收到事件或訊息後直接 加入到佇列,而處理執行緒讀取佇列中的事件或訊息,並加以處理。在這個模式中,有乙個線 程負責寫,多個處理執行緒讀自己的佇列並處理。雖然看起來象是一寫多讀,其實不然,針對 某一事件佇列而言,只有乙個執行緒是寫乙個執行緒是讀。...

Linux下多執行緒試驗(一)

今天練習linux下的多執行緒 將inculde後面的改為 不然部落格顯示不出來 include pthread.h include stdio.h include sys time.h include string.h void thread void int main void for i 0 ...

Linux下的多執行緒程式設計(一) 建立執行緒

1.執行緒的定義 乙個程序內部的控制序列。2.執行緒的優點 3.執行緒的缺點 4.執行緒的結構 執行緒的id 暫存器值,棧,排程優先順序和策略,訊號遮蔽字,errno 變數,私有資料。程序對他下面的所有執行緒共享了 可執行程式文字 程式的全域性記憶體和堆記憶體 作業系統分配的 棧以及檔案描述符。1....