日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

深入探索并发编程之内存屏障:资源控制操作

發布時間:2025/3/21 编程问答 19 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入探索并发编程之内存屏障:资源控制操作 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

當你使用資源控制時, 那么你肯定在試圖理解內存執行順序。不管你是用C,C++還是其它語言,這都是在編寫無鎖(lock-free)代碼時需要重點考慮的。

在上一篇文章中,我們介紹了編譯期間的內存亂序,這一部分內容構成內存執行順序問題的一部分。這篇文章講述另一部分:處理器本身在運行期間的內存執行順序。與編譯器亂序一樣,處理器亂序對于單線程來說也是不可見的。只有在使用無鎖(lock-free)技術時-也就是說,當共享內存在線程之間不是互斥量時,亂序現象才會變得顯露無疑。然而,與編譯器亂序不同的是,?處理器亂序只在多核和多處理器系統中才可見?.

你可以使用任意能充當memory barrier的指令來確保執行正確的內存順序。某種程度上來說,這是你需要了解的唯一技術,因為當你使用這類指令時,能自動處理好編譯器執行順序問題。充當memory barrier的指令包括(但不局限于)以下情況?注1?:

  • GCC中的內聯匯編指令,比如PowerPC平臺下的 asm volatile(“lwsync” ::: “memory”)
  • 除Xbox 360平臺以外,任意的Win32 Interlocked操作,
  • C++11原子類型操作,比如load(std::memory_order_acquire)
  • POSIX 互斥量操作,比如 pthread_mutex_lock

正因為有許多指令可以充當memory barrier,我們需要去了解許多不同類型的memory barrier. 實際上,上述的所有指令產生的memory barrier都屬于不同類型,這會寫lock-free代碼時給你帶來困惑。為了試圖把事情說清楚,我會把那些有助于理解大部分(但不是全部)可能的memory barrier類型做一個類比。

首先,考慮一種典型的多核系統結構。雙核,每個核上有32KiB的L1數據緩存,兩個核之間有1MiB的L2共享緩存, 主內存512MiB?注2?.

多核系統就有點像是一群程序員通過一種怪異的資源控制策略來合作一個項目。舉個例子,上面的雙核系統對應兩個程序員合作的場景。將這兩個程序員分別取名為Larry與Sergey.

右邊有個共享的中心倉庫-代表主內存和共享L2緩存的結合體。Larry和Sergey在本地機器中都有倉庫的工作副本,分別是每個CPU核中的L1緩存。每臺機器上有個臨時存儲區,用來記錄寄存器和本地變量的值?,F在兩個程序員坐在那里,富有激情的編輯他們的工作副本和臨時存儲區,根據他們看見的數據決定下一步該做什么–就像是一個線程就在那個CPU核上工作一樣。

在這開始引入資源控制策略。在這個類比里,資源控制策略實際上是非常奇怪的。由于Larry和Sergey修改了倉庫的工作副本,他們的修改在背后不斷地從倉庫中來回傳播(leaking),這種情況發生的次數都是隨機的。只要larry編輯文件X,對文件的修改都會泄露到倉庫中,但不能保證什么時候才會發生。有可能會立即發生,有可能會發生很多次,也有可能之后才發生。這時,他可能會繼續編輯其它的文件,比如Y和Z,這些改變可能會在X泄露之前就已經傳播了。這樣一來,寫操作在寫進倉庫的過程中就很容易發生亂序。

類似的,在Sergey的機器上,不能保證那些修改從倉庫到工作副本中來回傳播的時間間隔和順序。這樣一來,從倉庫讀數據的過程中就很容易發生亂序。

現在,如果每個程序員分別獨自工作在倉庫的隔離區域,他們都不會意識到背后的傳播過程,甚至不知道另一個程序員的存在。這就好比兩個正在運行的獨立的單線程。在這個例子中,內存執行順序的基本原則還是能保證的。

當程序員開始在倉庫的同一區域工作時,上述的類比就顯得更加重要了。再來看看之前文章中的例子。 X和Y是全局變量,且都初始化為0.

把X和Y想象成是Larry的工作副本,Sergey的工作副本以及倉庫本身中的文件。Larry將1寫入工作副本中的X,sergey同時將1寫進工作副本中的Y。 如果每個程序員在查詢工作副本中的其它文件之前,兩者的修改都能傳回到倉庫中,最后都會得到這個結果:r1 = 0 和 r2 = 0

。起初可能會覺得這種結果與直覺上相反,但對照下資源控制的類比方式,就覺得很明顯了。

Memory Barrier類型

幸運的是,Larry和Sergey不完全是對這種隨機性與在背后發生的不可預計的傳播無能為力。他們能發出一些特殊指令,調用fence指令來充當memory barrier). 對于上述類比方式,可以定義四種memory barrier,因此也對應四種fence指令。每種memory barrier都是以阻止不同類型的內存亂序能力來命名,比如,StoreLoad用來阻止寫讀類型。

就像?Doug?指出的那樣,這四種類型可能并不能很好的對應真實CPU中的特殊指令。大部分情況,一條CPU指令能充當上述幾種memory barrier類型的組合,可能也會附帶一些其它的效果。不論在什么情況下,只要你以資源控制的類比方式理解了這四種memory barrier,就容易理解真實CPU中的大多數的指令與一些高級編程語言的構造。

LoadLoad

一個LoadLoad barrier能有效地阻止在barrier之前執行的讀與在barrier之后執行的讀造成的亂序。

在我們的類比中,LoadLoad fence 指令基本等價于倉庫的的pull操作。想象git pull, hg pull, p4 sync, svn update 或者cvs update, 所有操作都在倉庫工作。如果本地的修改有任何的merge沖突,我們就說他們被隨機的決議(resolved)。

要提醒你的是,不能保證LoadLoad會pull整個倉庫的最近(或主干)的修訂版本。只要主干版本至少是從中心倉庫傳播到本地機器的最新值,就能pull比主干版本更老的版本。

這聽起來可能像一個較弱的保證,但仍然是一個完美的方案來阻止讀取過時的數據??紤]一個經典的例子,Sergey檢查共享flag來看Larry是不是發布了一些數據。如果flag為真,在讀取發布的數據之前,Sergey發出一個LoadLoad barrier 。

if (IsPublished) // Load and check shared flag {LOADLOAD_FENCE(); // Prevent reordering of loadsreturn Value; // Load published value } Load and check shared flag {LOADLOAD_FENCE(); // Prevent reordering of loadsreturn Value; // Load published value }

顯然,這個例子依賴于IsPublished標志位是否傳播到了Sergey的工作副本。不用去關心這些是什么時候發生的。只要發現了傳播的flag,它就發出一個LOadLoad fence來阻止讀取Value的值(這個值比flag本身還要老).

StoreStore

一個StoreStore barrier能有效的阻止在barrier之前的寫操作與在barrier之后的寫操作之間的亂序。

在我們的類比中,StoreStore fence指令對應倉庫的push操作。想象git push, hg push, p4 submit, svn commit or cvs commit 都發生在整個倉庫中。

跟繞口令一樣,假設StoreStore指令不是即時的,而是以異步的方式延后執行。因此,盡管Larry執行了StoreStore指令,我們對于他之前所有的寫操作什么時候能再在倉庫中出現不能做任何的假設。

這聽起來可能也像是弱的保證,但是,已經足夠來阻止Sergey收到Larry發布的任何過時的數據。 回到上面同樣的例子, 這時Larry只需要發布一些數據到共享內存,發出一個 StoreStore barrier,然后將共享flag設置為true

Value = x; // Publish some data STORESTORE_FENCE(); IsPublished = 1; // Set shared flag to indicate availability of data Set shared flag to indicate availability of data

再說一次,我們依賴從Larry的工作副本中傳播到Sergey的Ispublished值. 一旦Sergey檢測到,他相信自己看到了Value的正確值。有趣的是,在這種工作模式中,Value不用是原子類型,也可以是有許多元素的大結構體。

LoadStore

不像LoadLoad和StoreStore,就資源控制操作來看,LoadStore沒有比較合適的類比。 理解LoadStore的最好方法很簡單,就是考慮指令亂序。

想象Larry有一系列指令要執行。某些指令讓他從自己的工作副本讀取數據到寄存器中,以及某些指令從寄存器中寫數據到工作副本中。Larry具備欺騙指令的能力,但只限于一些特殊場合。只要他遇到讀操作時,他就會先檢測讀操作之后的任何寫操作。如果寫操作和當前的讀操作完全不相關,他會先略過讀操作,先進行寫操作,結束后在進行讀操作。在這種場景下,內存執行順序的基本原則–絕不修改單程序的行為–仍然是遵守了的。

對于真實CPU,如果某些處理器中有一個緩存錯過了讀操作(緊接著緩存命中的寫操作),這種指令亂序就可能會發生。為了理解這個類比,硬件細節并不重要。我們只當做Larry的工作很繁瑣,而這正是他能創新的機會(這種機會很有限)。 不管他是否選擇這么做都是完全不可預計的。幸運的是,阻止這種亂序類型的開銷并不大。當Larry遇到一個LoadStore barrier, 他就能簡單的避免barrier附近的亂序。

在我們的類比中,盡管在讀寫之間有個LoadLoad或者StoreStore barrier時,Larry執行這種類型的LoadStore亂序也是有效的。 然而,在真實的CPU中,充當LoadStore barrier的指令至少能充當其它兩種類型的barrier。

StoreLoad

StoreLoad barrier能確保其它處理器遇到barrier之前執行所有的寫操作,另外,barrier之后執行的所有讀操作能收到最近能被barrier可見的值。 .換句話說,它能在barrier處理所有的讀操作之前有效地阻止所有的寫操作亂序,尊重順序一致性多處理器執行那些操作的方式。

StoreLoad是唯一的。這是唯一的一種memory barrier類型可以阻止前文提到的這種結果:r1 = r2 = 0,這個例子我在前面的文章中提到過很多次了。

如果你一路仔細地讀下來,可能會有個疑惑: StoreLoad和 StoreStore再緊接著一個LoadLoad有什么不一樣呢?畢竟,StoreStore 將修改push到倉庫中,然而LoadLoad 把遠程的修改pull回來。然而,那兩種barrier類型是不夠的。記住,push操作可能會因為任意數量的指令而延遲, pull操作可能不會從主干版本中pull數據。 這也解釋了為什么owerPC的lwsync指令–充當所有LoadLoad, LoadStore和StoreStore memory barrier,但不是StoreLoad–是不足夠來阻止例子中的r1 = r2 = 0 這種結果的。

拿類比來說,StoreLoad barrier能通過push所有局部修改到倉庫中來實現,等待那個操作完成,然后pull 倉庫中絕對的、最近的主干修訂版本。在大部分處理器上,充當StoreLoad barrier的指令比充當其它類型barrier的指令開銷更大。

如果在那個操作中放置一個LoadStore barrier,也沒什么大不了的, 之后我們得到的是一個完整的memory fence–一次性充當所有四種barrier 類型。 正如?Doug?指出的那樣,在目前所有處理器上都是這樣,每個充當StoreLoad barrier的指令也充當完整的memory fence.

類比給你帶來了什么?

正如我之前提到的那樣,在處理內存執行順序時,每個處理器都有不同的?特點?。具體來說,在x86/64家族中有一種強內存模型。這是為了讓內存亂序的發生降到最低。PowerPC和ARM有著弱的內存模型。Alpha因自成一派而出名。幸運的是,這篇文章的類比對應一種弱的?內存模型?。 如果你能用心對待它,并使用這里提供的fence指令來實施正確的內存執行順序,你就能處理大部分的CPU。

這種類比同樣對應針對C++11和C11的抽象機器。因此,如果你使用那些語言的標準庫寫無鎖(lock-free)代碼,同時將上述的類比記在腦里,就更有可能在任何平臺下正確執行。

在這種類比中,我說過,每個程序員代表在一個核心中正在運行的單線程。在一個真實的操作系統中,線程更傾向在生命周期中在不同的核心里移動,這時上述的類比仍然有效。我也在機器語言和C/C++語言不斷變換來舉例。顯然,我們更傾向使用C/C++,或者另外更高級的語言。這是有可能的,因為任何充當memory barrier的操作也能阻止編譯器亂序。

我還沒有寫關于每種memory barrier類型的文章。例如,也存在數據依賴(data dependency )barriers?注3?。 我會在以后的文章中講這些內容。然而,上面給出的四種類型仍是最重要的。

如果你對CPU在底層是如何工作的很感興趣–像寫緩存,緩存一致性協議(cache coherency protocol)以及硬件實施細節相關的東西–以及為什么它們為什么會發生內存亂序注4?,我推薦Paul Paul McKenney & David Howell的?工作?。 實際上,我認為能成功寫好無鎖(lock-free)代碼的大部分程序員都至少都對這種硬件細節有點熟悉。

注釋

注1:gcc+x86下,編譯器級別的memory barrier和CPU級別的memory barrier可以如下實現:

define COMPILER_BARRIER() __asm__ __volatile__("" : : : "memory") define CPU_BARRIER() __sync_synchronize COMPILER_BARRIER() __asm__ __volatile__("" : : : "memory") define CPU_BARRIER() __sync_synchronize

其中,?CPU_BARRIER()?可以防止CPU寫讀、寫寫、讀寫、讀讀亂序。如你所知,CPU級別的memory barrier同時約束CPU和編譯器的亂序;而編譯器級別的memory barrier只約束編譯器的亂序,不影響CPU。

注2:現在用的很多的cpu一般有三級cache。

注3:為了演示data dependency barrier,考慮以下例子:

初始化 int A = 1; int B = 2; int C = 3; int *P = &A; int *Q = &B; //cpu1 B = 4; CPU_BARRIER(); P = &B; //cpu2 Q = P; D = *Q int A = 1; int B = 2; int C = 3; int *P = &A; int *Q = &B; //cpu1 B = 4; CPU_BARRIER(); P = &B; //cpu2 Q = P; D = *Q

從直覺上說,Q最后要么等于&A,要么等于&B。也就是說:

Q == &A, D == 1

或者

Q == &B, D == 4

但是,讓人吃驚的是,在某些CPU體系結構例如DEC Alpha下,可能出現

Q == &B, D == 2的情形,也就是說執行順序(total order,global order)是:

D = *Q B = 4; P = &B; Q = P; = *Q B = 4; P = &B; Q = P;

也就是說CPU2的兩行賦值發生了亂序,而我們知道,這里是存在數據依賴的,順序非常關鍵。因此,這里需要加一個data dependency barrier。

注4:為什么CPU亂序只在多核多線程下才可能會暴露出問題?為什么X86體系結構的Intel CPU要對寫讀進行亂序?

要明白這兩個問題,我們首先得知道cache coherency,也就是所謂的cache一致性。

在現代計算機里,一般包含至少三種角色:cpu、cache、內存。一般說來,內存只有一個;CPU Core有多個;cache有多級,cache的基本塊單位是cacheline,大小一般是64B-256B。

每個cpu core有自己的私有的cache(有一級cache是共享的,如文中所示),而cache只是內存的副本。那么這就帶來一個問題:如何保證每個cpu core中的cache是一致的?

在廣泛使用的cache一致性協議即MESI協議中,cacheline有四種狀態:Modified、Exclusive、Shared、Invalid,分別表示修改、獨占、共享、無效。

當某個cpu core寫一個內存變量時,往往是(先)只修改cache,那么這就會導致不一致。為了保證一致,需要先把其他core的對應的cacheline都invalid掉,給其他core們發送invalid消息,然后等待它們的response。

這個過程是耗時的,需要執行寫變量的core等待,阻塞了它后面的操作。為了解決這個問題,cpu core往往有自己專屬的store buffer。

等待其他core給它response的時候,就可以先寫store buffer,然后繼續后面的讀操作,對外表現就是寫讀亂序。

因為寫操作是寫到store buffer中的,而store buffer是私有的,對其他core是透明的,core1無法訪問core2的store buffer。因此其他core讀不到這樣的修改。

這就是大概的原理。MESI協議非常復雜,背后的技術也很有意思。

總結

以上是生活随笔為你收集整理的深入探索并发编程之内存屏障:资源控制操作的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。