[英]C++11 atomic memory ordering - is this a correct usage of relaxed (release-consume) ordering?
我最近使用std :: atomic三重緩沖區為C ++ 11創建了一個端口,用作並發同步機制。 這種線程同步方法背后的想法是,對於生產者 - 消費者情況,你有一個運行速度更快的生產者,消費者,三重緩沖可以帶來一些好處,因為生產者線程不會因為必須等待而“減慢”速度對於消費者。 在我的例子中,我有一個物理線程,在~120fps時更新,以及一個以~60fps運行的渲染線程。 顯然,我希望渲染線程始終能夠獲得最新狀態,但我也知道我將從物理線程中跳過很多幀,因為速率不同。 另一方面,我希望我的物理線程保持其不變的更新速率,而不受鎖定我的數據的較慢渲染線程的限制。
最初的C代碼是由remis-ideas制作的,完整的解釋在他的博客中 。 我鼓勵任何有興趣閱讀它的人進一步了解原始實現。
我的實現可以在這里找到。
基本思想是使一個具有3個位置(緩沖區)的數組和一個原子標志進行比較和交換,以定義在任何給定時間哪些數組元素對應於什么狀態。 這樣,只有一個原子變量用於模擬數組的所有3個索引和三重緩沖背后的邏輯。 緩沖區的3個位置被命名為Dirty,Clean和Snap。 生產者總是寫入Dirty索引,並且可以翻轉writer以將Dirty與當前的Clean索引交換。 消費者可以請求一個新的Snap,它將當前的Snap索引與Clean索引交換以獲得最新的緩沖區。 消費者總是在Snap位置讀取緩沖區。
該標志由8位無符號整數組成,這些位對應於:
(未使用)(新寫)(2x臟)(2x清潔)(2x快照)
newWrite extra bit標志由寫入器設置並由讀取器清除。 讀者可以使用它來檢查自上次捕捉以來是否有任何寫入,如果不是,則不會再次捕捉。 可以使用簡單的按位運算獲得標志和索引。
現在好了代碼:
template <typename T>
class TripleBuffer
{
public:
TripleBuffer<T>();
TripleBuffer<T>(const T& init);
// non-copyable behavior
TripleBuffer<T>(const TripleBuffer<T>&) = delete;
TripleBuffer<T>& operator=(const TripleBuffer<T>&) = delete;
T snap() const; // get the current snap to read
void write(const T newT); // write a new value
bool newSnap(); // swap to the latest value, if any
void flipWriter(); // flip writer positions dirty / clean
T readLast(); // wrapper to read the last available element (newSnap + snap)
void update(T newT); // wrapper to update with a new element (write + flipWriter)
private:
bool isNewWrite(uint_fast8_t flags); // check if the newWrite bit is 1
uint_fast8_t swapSnapWithClean(uint_fast8_t flags); // swap Snap and Clean indexes
uint_fast8_t newWriteSwapCleanWithDirty(uint_fast8_t flags); // set newWrite to 1 and swap Clean and Dirty indexes
// 8 bit flags are (unused) (new write) (2x dirty) (2x clean) (2x snap)
// newWrite = (flags & 0x40)
// dirtyIndex = (flags & 0x30) >> 4
// cleanIndex = (flags & 0xC) >> 2
// snapIndex = (flags & 0x3)
mutable atomic_uint_fast8_t flags;
T buffer[3];
};
執行:
template <typename T>
TripleBuffer<T>::TripleBuffer(){
T dummy = T();
buffer[0] = dummy;
buffer[1] = dummy;
buffer[2] = dummy;
flags.store(0x6, std::memory_order_relaxed); // initially dirty = 0, clean = 1 and snap = 2
}
template <typename T>
TripleBuffer<T>::TripleBuffer(const T& init){
buffer[0] = init;
buffer[1] = init;
buffer[2] = init;
flags.store(0x6, std::memory_order_relaxed); // initially dirty = 0, clean = 1 and snap = 2
}
template <typename T>
T TripleBuffer<T>::snap() const{
return buffer[flags.load(std::memory_order_consume) & 0x3]; // read snap index
}
template <typename T>
void TripleBuffer<T>::write(const T newT){
buffer[(flags.load(std::memory_order_consume) & 0x30) >> 4] = newT; // write into dirty index
}
template <typename T>
bool TripleBuffer<T>::newSnap(){
uint_fast8_t flagsNow(flags.load(std::memory_order_consume));
do {
if( !isNewWrite(flagsNow) ) // nothing new, no need to swap
return false;
} while(!flags.compare_exchange_weak(flagsNow,
swapSnapWithClean(flagsNow),
memory_order_release,
memory_order_consume));
return true;
}
template <typename T>
void TripleBuffer<T>::flipWriter(){
uint_fast8_t flagsNow(flags.load(std::memory_order_consume));
while(!flags.compare_exchange_weak(flagsNow,
newWriteSwapCleanWithDirty(flagsNow),
memory_order_release,
memory_order_consume));
}
template <typename T>
T TripleBuffer<T>::readLast(){
newSnap(); // get most recent value
return snap(); // return it
}
template <typename T>
void TripleBuffer<T>::update(T newT){
write(newT); // write new value
flipWriter(); // change dirty/clean buffer positions for the next update
}
template <typename T>
bool TripleBuffer<T>::isNewWrite(uint_fast8_t flags){
// check if the newWrite bit is 1
return ((flags & 0x40) != 0);
}
template <typename T>
uint_fast8_t TripleBuffer<T>::swapSnapWithClean(uint_fast8_t flags){
// swap snap with clean
return (flags & 0x30) | ((flags & 0x3) << 2) | ((flags & 0xC) >> 2);
}
template <typename T>
uint_fast8_t TripleBuffer<T>::newWriteSwapCleanWithDirty(uint_fast8_t flags){
// set newWrite bit to 1 and swap clean with dirty
return 0x40 | ((flags & 0xC) << 2) | ((flags & 0x30) >> 2) | (flags & 0x3);
}
如您所見,我決定使用Release-Consume模式進行內存排序。 為存儲所述釋放 (memory_order_release)確保在當前線程沒有寫入可以在商店后重新排序。 另一方面, Consume確保當前線程中的讀取不依賴於當前加載的值,可以在此加載之前重新排序。 這確保了在當前線程中可以看到對釋放相同原子變量的其他線程中的因變量的寫入。
如果我的理解是正確的,因為我只需要原子設置標志,對其他不直接影響標志的變量的操作可以由編譯器自由重新排序,允許更多的優化。 通過閱讀新內存模型上的一些文檔,我也意識到這些輕松的原子只會對ARM和POWER等平台產生明顯的影響(主要是因為它們而引入)。 由於我的目標是ARM,我相信我可以從這些操作中受益,並能夠將性能提高一點。
現在提問:
我是否正確使用了Release-Consume這個特定問題的輕松排序?
謝謝,
安德烈
PS:對於長篇文章感到抱歉,但我認為需要一些不錯的背景來更好地了解問題。
編輯:實施@Yakk的建議:
newSnap()
和flipWriter()
上讀取的固定flags
使用直接賦值,因此使用默認load(std::memory_order_seq_cst)
。 bool
返回類型到newSnap()
,現在返回false時沒有新的,否則為true。 = delete
idiom將類定義為不可復制的,因為如果使用TripleBuffer
,復制和賦值構造函數都是不安全的。 編輯2:修正了描述,這是不正確的(謝謝@Useless)。 消費者請求新的Snap並從Snap索引(而不是“writer”)讀取。 抱歉分心,感謝Useless指出來。
編輯3:根據@Display Name的建議優化newSnap()
和flipriter()
函數,有效地在每個循環周期中刪除2個冗余load()
。
為什么要在CAS循環中加載舊標志值兩次? 第一次是flags.load()
,第二次是compare_exchange_weak()
,標准在CAS失敗時指定將第二個參數加載到第一個參數,在本例中是flagsNow。
根據http://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange ,“ 否則,將存儲在* this中的實際值加載到預期值(執行加載操作)。 ”那么你的循環正在做的是失敗, compare_exchange_weak()
重載flagsNow
,則循環重復,並且第一個語句加載它再次,通過負載后立即compare_exchange_weak()
在我看來,你的循環應該將負載拉出循環。 例如, newSnap()
將是:
uint_fast8_t flagsNow(flags.load(std::memory_order_consume));
do
{
if( !isNewWrite(flagsNow)) return false; // nothing new, no need to swap
} while(!flags.compare_exchange_weak(flagsNow, swapSnapWithClean(flagsNow), memory_order_release, memory_order_consume));
和flipWriter()
:
uint_fast8_t flagsNow(flags.load(std::memory_order_consume));
while(!flags.compare_exchange_weak(flagsNow, newWriteSwapCleanWithDirty(flagsNow), memory_order_release, memory_order_consume));
是的,它是memory_order_acquire和memory_order_consume之間的區別,但是當你每秒使用180左右時你不會注意到它。 如果你想知道數字的答案,你可以使用m2 = memory_order_consume運行我的測試。 只需將producer_or_consumer_Thread更改為:
TripleBuffer <int> tb;
void producer_or_consumer_Thread(void *arg)
{
struct Arg * a = (struct Arg *) arg;
bool succeeded = false;
int i = 0, k, kold = -1, kcur;
while (a->run)
{
while (a->wait) a->is_waiting = true; // busy wait
if (a->producer)
{
i++;
tb.update(i);
a->counter[0]++;
}
else
{
kcur = tb.snap();
if (kold != -1 && kcur != kold) a->counter[1]++;
succeeded = tb0.newSnap();
if (succeeded)
{
k = tb.readLast();
if (kold == -1)
kold = k;
else if (kold = k + 1)
kold = k;
else
succeeded = false;
}
if (succeeded) a->counter[0]++;
}
}
a->is_waiting = true;
}
測試結果:
_#_ __Produced __Consumed _____Total
1 39258150 19509292 58767442
2 24598892 14730385 39329277
3 10615129 10016276 20631405
4 10617349 10026637 20643986
5 10600334 9976625 20576959
6 10624009 10069984 20693993
7 10609040 10016174 20625214
8 25864915 15136263 41001178
9 39847163 19809974 59657137
10 29981232 16139823 46121055
11 10555174 9870567 20425741
12 25975381 15171559 41146940
13 24311523 14490089 38801612
14 10512252 9686540 20198792
15 10520211 9693305 20213516
16 10523458 9720930 20244388
17 10576840 9917756 20494596
18 11048180 9528808 20576988
19 11500654 9530853 21031507
20 11264789 9746040 21010829
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.