99精品伊人亚洲|最近国产中文炮友|九草在线视频支援|AV网站大全最新|美女黄片免费观看|国产精品资源视频|精彩无码视频一区|91大神在线后入|伊人终合在线播放|久草综合久久中文

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

如何實(shí)現(xiàn)一個(gè)多讀多寫的線程安全的無鎖隊(duì)列

科技綠洲 ? 來源:Linux開發(fā)架構(gòu)之路 ? 作者:Linux開發(fā)架構(gòu)之路 ? 2023-11-08 15:25 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

在ZMQ無鎖隊(duì)列的原理與實(shí)現(xiàn)一文中,我們已經(jīng)知道了ypipe可以實(shí)現(xiàn)一線程寫一線程讀的無鎖隊(duì)列,那么其劣勢就很明顯了,無法適應(yīng)多寫多讀的場景,因?yàn)槠湓谧x的時(shí)候沒有對(duì)r指針加鎖,在寫的時(shí)候沒有對(duì)w指針加鎖。那么如何實(shí)現(xiàn)一個(gè)多讀多寫的線程安全的無鎖隊(duì)列呢?

  1. 互斥鎖:mutexqueue(太簡單不介紹了)
  2. 互斥鎖+條件變量:blockqueue(太簡單不介紹了)
  3. 內(nèi)存屏障:lockfreequeue(SimpleLockFreeQueue.h 暫時(shí)未寫文章介紹)
  4. 雙重CAS原子操作:arraylockfreequeue(本文)

圖片

2. ArrayLockFreeQueue的類接?和變量

該程序使用 gcc 內(nèi)置的__sync_bool_compare_and_swap,但重新做了宏定義封裝。

#define CAS(a_ptr, a_oldVal, a_newVal) __sync_bool_compare_and_swap(a_ptr, a_oldVal, a_newVal)

所謂循環(huán)數(shù)組,就是RingBuffer

圖片

#define QUEUE_INT unsigned long
#define ARRAY_LOCK_FREE_Q_DEFAULT_SIZE 65535 // 2^16

template
class ArrayLockFreeQueue {
public:

ArrayLockFreeQueue();

virtual ~ArrayLockFreeQueue();

QUEUE_INT size();

bool enqueue(const ELEM_T &a_data);// ?隊(duì)列

bool dequeue(ELEM_T &a_data);// 出隊(duì)列

private:

ELEM_T m_thequeue[Q_SIZE];

volatile QUEUE_INT m_count;// 隊(duì)列內(nèi)有多少元素
volatile QUEUE_INT m_writeIndex;//新元素?列時(shí)存放位置在數(shù)組中的下標(biāo)

volatile QUEUE_INT m_readIndex;//下?個(gè)出列元素在數(shù)組中的下標(biāo)

volatile QUEUE_INT m_maximumReadIndex;最后?個(gè)已經(jīng)完成?列操作的元素在數(shù)組中的下標(biāo)

//取余
inline QUEUE_INT countToIndex(QUEUE_INT a_count);
};

2.1 變量介紹

  • m_count:隊(duì)列的元素個(gè)數(shù)
  • m_writeIndex:新元素?列時(shí)存放位置在數(shù)組中的下標(biāo)
  • m_readIndex:下?個(gè)出列元素在數(shù)組中的下標(biāo)
  • m_maximumReadIndex:最后?個(gè)已經(jīng)完成?列操作的元素在數(shù)組中的下標(biāo)。如果它的值跟m_writeIndex不?致,表明有寫請(qǐng)求尚未完成。這意味著,有寫請(qǐng)求成功申請(qǐng)了空間但數(shù)據(jù)還沒完全寫進(jìn)隊(duì)列。所以如果有線程要讀取,必須要等到寫線程將數(shù)據(jù)完全寫?到隊(duì)列之后。

必須指明的是使?3種不同的下標(biāo)都是必須的,因?yàn)殛?duì)列允許任意數(shù)量的?產(chǎn)者和消費(fèi)者圍繞著它?作。已經(jīng)存在?種基于循環(huán)數(shù)組的?鎖隊(duì)列,使得唯?的?產(chǎn)者和唯?的消費(fèi)者可以良好的?作。它的實(shí)現(xiàn)相當(dāng)簡潔?常值得閱讀。

2.2 函數(shù)介紹

2.2.1 取余函數(shù)QUEUE_INT countToIndex(QUEUE_INT a_count);

這個(gè)函數(shù)非常有用,因?yàn)槲覀儗?shí)現(xiàn)的是循環(huán)隊(duì)列,所以一定要對(duì)數(shù)組長度取余。

template
inline QUEUE_INT ArrayLockFreeQueue::countToIndex(QUEUE_INT a_count) {
return (a_count % Q_SIZE); // 取余的時(shí)候
},>

隊(duì)列已滿判斷如下

// (m_writeIndex + 1) %/Q_SIZE == m_readIndex
countToIndex(currentWriteIndex + 1) == countToIndex(currentReadIndex)

隊(duì)列為空判斷如下

//m_readIndex == m_maximumReadIndex
countToIndex(currentReadIndex) == countToIndex(currentMaximumReadIndex)

相關(guān)視頻推薦

C++無鎖隊(duì)列的設(shè)計(jì)與實(shí)現(xiàn)

高并發(fā)場景下,三種鎖方案:互斥鎖,自旋鎖,原子操作的優(yōu)缺點(diǎn)

需要C/C++ Linux服務(wù)器架構(gòu)師學(xué)習(xí)資料加qun812855908獲取(資料包括C/C++,Linux,golang技術(shù),Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協(xié)程,DPDK,ffmpeg等),免費(fèi)分享

圖片

2.2.2 文字舉例

2.2.2.1 入隊(duì)函數(shù)bool enqueue(const ELEM_T &a_data);

下面以文字舉例說明函數(shù),后續(xù)再圖示舉例。

假設(shè)現(xiàn)在有兩個(gè)線程都進(jìn)入了enqueue這個(gè)函數(shù),m_writeIndex,m_readIndex和m_maximumReadIndex都為0。

  • 線程1在第一個(gè)while和CAS處:
currentWriteIndex = 0
currentReadIndex = 0
CAS(0,0,1)----> m_writeIndex = 1
m_maximumReadIndex = 0
  • 線程2在第一個(gè)while和CAS處:
currentWriteIndex = 0
currentReadIndex = 0
CAS(1,0,1)----> m_writeIndex = m_writeIndex false

while再次循環(huán)

currentWriteIndex = 1
currentReadIndex = 0
CAS(1,1,2)----> m_writeIndex = 2
m_maximumReadIndex = 0
  • 線程2在第二個(gè)while和CAS處:
CAS(0,1,2) -----> m_maximumReadIndex = m_maximumReadIndex false
yidld讓出CPU
此時(shí)線程1執(zhí)行↓
  • 線程1在第二個(gè)while和CAS處:
CAS(0,0,1) ------>m_maximumReadIndex = 1
執(zhí)行結(jié)束,此時(shí)線程2恢復(fù)執(zhí)行
  • 線程2在第二個(gè)while和CAS處:
CAS(1,1,2) ------>m_maximumReadIndex = 2
執(zhí)行結(jié)束
template
bool ArrayLockFreeQueue::enqueue(const ELEM_T &a_data) {
QUEUE_INT currentWriteIndex; // 獲取寫指針的位置
QUEUE_INT currentReadIndex;
// 1. 獲取可寫入的位置
do {
currentWriteIndex = m_writeIndex;
currentReadIndex = m_readIndex;
if (countToIndex(currentWriteIndex + 1) ==
countToIndex(currentReadIndex)) {
return false; // 隊(duì)列已經(jīng)滿了
}
// 目的是為了獲取一個(gè)能寫入的位置
} while (!CAS(&m_writeIndex, currentWriteIndex, (currentWriteIndex + 1)));
// 獲取寫入位置后 currentWriteIndex 是一個(gè)臨時(shí)變量,保存我們寫入的位置
// We know now that this index is reserved for us. Use it to save the data
m_thequeue[countToIndex(currentWriteIndex)] = a_data; // 把數(shù)據(jù)更新到對(duì)應(yīng)的位置

// 2. 更新可讀的位置,按著m_maximumReadIndex+1的操作
// update the maximum read index after saving the data. It wouldn't fail if there is only one thread
// inserting in the queue. It might fail if there are more than 1 producer threads because this
// operation has to be done in the same order as the previous CAS
while (!CAS(&m_maximumReadIndex, currentWriteIndex, (currentWriteIndex + 1))) {
// this is a good place to yield the thread in case there are more
// software threads than hardware processors and you have more
// than 1 producer thread
// have a look at sched_yield (POSIX.1b)
sched_yield(); // 當(dāng)線程超過cpu核數(shù)的時(shí)候如果不讓出cpu導(dǎo)致一直循環(huán)在此。
}
//printf("m_writeIndex:%d currentWriteIndex:%d m_maximumReadIndex:%dn",m_writeIndex,currentWriteIndex,m_maximumReadIndex);
AtomicAdd(&m_count, 1);

return true;
},>

2.2.2.2 出隊(duì)函數(shù)bool dequeue(ELEM_T &a_data);

下面以文字舉例說明函數(shù),后續(xù)再圖示舉例。

假設(shè)現(xiàn)在有兩個(gè)線程都進(jìn)入了dequeue這個(gè)函數(shù),currentReadIndex為0,currentMaximumReadIndex為2。

  • 線程1執(zhí)行
currentReadIndex = 0
currentMaximumReadIndex = 2
data = m_thequeue[0]
CAS(0,0,1) ----> m_readIndex = 1
  • 線程2執(zhí)行
currentReadIndex = 0
currentMaximumReadIndex = 2
data = m_thequeue[0]
CAS(1,0,1) ----> m_readIndex = m_readIndex false

while再循環(huán)

currentReadIndex = 1
currentMaximumReadIndex = 2
data = m_thequeue[1]
CAS(1,1,2) ----> m_readIndex = 2

如果沒有新數(shù)據(jù)寫入,再次讀取數(shù)據(jù),則currentReadIndex(2)==currentMaximumReadIndex(2)相等,return false,沒有數(shù)據(jù)可讀。

template
bool ArrayLockFreeQueue::dequeue(ELEM_T &a_data) {
QUEUE_INT currentMaximumReadIndex;
QUEUE_INT currentReadIndex;

do {
// to ensure thread-safety when there is more than 1 producer thread
// a second index is defined (m_maximumReadIndex)
currentReadIndex = m_readIndex;
currentMaximumReadIndex = m_maximumReadIndex;

if (countToIndex(currentReadIndex) ==
countToIndex(currentMaximumReadIndex)) // 如果不為空,獲取到讀索引的位置
{
// the queue is empty or
// a producer thread has allocate space in the queue but is
// waiting to commit the data into it
return false;
}
// retrieve the data from the queue
a_data = m_thequeue[countToIndex(currentReadIndex)]; // 從臨時(shí)位置讀取的

// try to perfrom now the CAS operation on the read index. If we succeed
// a_data already contains what m_readIndex pointed to before we
// increased it
if (CAS(&m_readIndex, currentReadIndex, (currentReadIndex + 1))) {
//printf("m_readIndex:%d currentReadIndex:%d m_maximumReadIndex:%dn",m_readIndex,currentReadIndex,m_maximumReadIndex);
AtomicSub(&m_count, 1); // 真正讀取到了數(shù)據(jù),元素-1
return true;
}
} while (true);

assert(0);
// Add this return statement to avoid compiler warnings
return false;
},>

2.2.3 圖示舉例

2.2.3.1 入隊(duì)函數(shù)bool enqueue(const ELEM_T &a_data);

下面的圖我就不分目錄了,直接一口氣說明完。

以下插圖展示了對(duì)隊(duì)列執(zhí)?操作時(shí)各下標(biāo)是如何變化的。如果?個(gè)位置被標(biāo)記為X,標(biāo)識(shí)這個(gè)位置?存放了數(shù)據(jù)。空?表示位置是空的。對(duì)于下圖的情況,隊(duì)列中存放了兩個(gè)元素。WriteIndex指示的位置是新元素將會(huì)被插?的位置。ReadIndex指向的位置中的元素將會(huì)在下?次pop操作中被彈出。

圖片

當(dāng)?產(chǎn)者準(zhǔn)備將數(shù)據(jù)插?到隊(duì)列中,它?先通過增加WriteIndex的值來申請(qǐng)空間。MaximumReadIndex指向最后?個(gè)存放有效數(shù)據(jù)的位置(也就是實(shí)際的隊(duì)列尾)。

圖片

?旦空間的申請(qǐng)完成,?產(chǎn)者就可以將數(shù)據(jù)拷?到剛剛申請(qǐng)到的位置中。完成之后增加MaximumReadIndex使得它與WriteIndex的?致。

圖片

現(xiàn)在隊(duì)列中有3個(gè)元素,接著?有?個(gè)?產(chǎn)者嘗試向隊(duì)列中插?元素。

圖片

在第?個(gè)?產(chǎn)者完成數(shù)據(jù)拷?之前,?有另外?個(gè)?產(chǎn)者申請(qǐng)了?個(gè)新的空間準(zhǔn)備拷?數(shù)據(jù)?,F(xiàn)在有兩個(gè)?產(chǎn)者同時(shí)向隊(duì)列插?數(shù)據(jù)。

圖片

現(xiàn)在?產(chǎn)者開始拷?數(shù)據(jù),在完成拷?之后,對(duì)MaximumReadIndex的遞增操作必須嚴(yán)格遵循?個(gè)順序:第?個(gè)?產(chǎn)者線程?先遞增MaximumReadIndex,接著才輪到第?個(gè)?產(chǎn)者。這個(gè)順序必須被嚴(yán)格遵守的原因是,我們必須保證數(shù)據(jù)被完全拷?到隊(duì)列之后才允許消費(fèi)者線程將其出列。讓出cpu的?的也是為了讓排在最前?的?產(chǎn)者完成m_maximumReadIndex的更新

while (!CAS(&m_maximumReadIndex, currentWriteIndex, (currentWriteIndex + 1))) {
sched_yield(); // 當(dāng)線程超過cpu核數(shù)的時(shí)候如果不讓出cpu導(dǎo)致一直循環(huán)在此。
}

圖片

第?個(gè)?產(chǎn)者完成了數(shù)據(jù)拷?,并對(duì)MaximumReadIndex完成了遞增,現(xiàn)在第?個(gè)?產(chǎn)者可以遞增MaximumReadIndex了。

圖片

第?個(gè)?產(chǎn)者完成了對(duì)MaximumReadIndex的遞增,現(xiàn)在隊(duì)列中有5個(gè)元素。

template
bool ArrayLockFreeQueue::enqueue(const ELEM_T &a_data) {
QUEUE_INT currentWriteIndex; // 獲取寫指針的位置
QUEUE_INT currentReadIndex;
// 1. 獲取可寫入的位置
do {
currentWriteIndex = m_writeIndex;
currentReadIndex = m_readIndex;
if (countToIndex(currentWriteIndex + 1) ==
countToIndex(currentReadIndex)) {
return false; // 隊(duì)列已經(jīng)滿了
}
// 目的是為了獲取一個(gè)能寫入的位置
} while (!CAS(&m_writeIndex, currentWriteIndex, (currentWriteIndex + 1)));
// 獲取寫入位置后 currentWriteIndex 是一個(gè)臨時(shí)變量,保存我們寫入的位置
// We know now that this index is reserved for us. Use it to save the data
m_thequeue[countToIndex(currentWriteIndex)] = a_data; // 把數(shù)據(jù)更新到對(duì)應(yīng)的位置

// 2. 更新可讀的位置,按著m_maximumReadIndex+1的操作
// update the maximum read index after saving the data. It wouldn't fail if there is only one thread
// inserting in the queue. It might fail if there are more than 1 producer threads because this
// operation has to be done in the same order as the previous CAS
while (!CAS(&m_maximumReadIndex, currentWriteIndex, (currentWriteIndex + 1))) {
// this is a good place to yield the thread in case there are more
// software threads than hardware processors and you have more
// than 1 producer thread
// have a look at sched_yield (POSIX.1b)
sched_yield(); // 當(dāng)線程超過cpu核數(shù)的時(shí)候如果不讓出cpu導(dǎo)致一直循環(huán)在此。
}
// printf("m_writeIndex:%d currentWriteIndex:%d m_maximumReadIndex:%dn",m_writeIndex,currentWriteIndex,m_maximumReadIndex);
AtomicAdd(&m_count, 1);

return true;
},>

2.2.3.2 出隊(duì)函數(shù)bool dequeue(ELEM_T &a_data);

以下插圖展示了元素出列的時(shí)候各種下標(biāo)是如何變化的,隊(duì)列中初始有2個(gè)元素。WriteIndex指示的位置是新元素將會(huì)被插?的位置。ReadIndex指向的位置中的元素將會(huì)在下?次pop操作中被彈出。

圖片

消費(fèi)者線程拷?數(shù)組ReadIndex位置的元素,然后嘗試?CAS操作將ReadIndex加1。如果操作成功消費(fèi)者成功的將數(shù)據(jù)出列。因?yàn)镃AS操作是原?的,所以只有唯?的線程可以在同?時(shí)刻更新ReadIndex的值。如果操作失敗,讀取新的ReadIndex值,以重復(fù)以上操作(copy數(shù)據(jù),CAS)。

圖片

現(xiàn)在?有?個(gè)消費(fèi)者將元素出列,隊(duì)列變成空。

圖片

現(xiàn)在有?個(gè)?產(chǎn)者正在向隊(duì)列中添加元素。它已經(jīng)成功的申請(qǐng)了空間,但尚未完成數(shù)據(jù)拷?。任何其它企圖從隊(duì)列中移除元素的消費(fèi)者都會(huì)發(fā)現(xiàn)隊(duì)列?空(因?yàn)閣riteIndex不等于readIndex)。但它不能讀取readIndex所指向位置中的數(shù)據(jù),因?yàn)閞eadIndex與MaximumReadIndex相等(相等break)。直到?產(chǎn)者完成數(shù)據(jù)拷?增加MaximumReadIndex的值才能讀取這個(gè)數(shù)據(jù)。

圖片

當(dāng)?產(chǎn)者完成數(shù)據(jù)拷?,隊(duì)列的??是1,消費(fèi)者線程可以讀取這個(gè)數(shù)據(jù)了。

template
bool ArrayLockFreeQueue::dequeue(ELEM_T &a_data) {
QUEUE_INT currentMaximumReadIndex;
QUEUE_INT currentReadIndex;

do {
// to ensure thread-safety when there is more than 1 producer thread
// a second index is defined (m_maximumReadIndex)
currentReadIndex = m_readIndex;
currentMaximumReadIndex = m_maximumReadIndex;

if (countToIndex(currentReadIndex) ==
countToIndex(currentMaximumReadIndex)) // 如果不為空,獲取到讀索引的位置
{
// the queue is empty or
// a producer thread has allocate space in the queue but is
// waiting to commit the data into it
return false;
}
// retrieve the data from the queue
a_data = m_thequeue[countToIndex(currentReadIndex)]; // 從臨時(shí)位置讀取的

// try to perfrom now the CAS operation on the read index. If we succeed
// a_data already contains what m_readIndex pointed to before we
// increased it
if (CAS(&m_readIndex, currentReadIndex, (currentReadIndex + 1))) {
//printf("m_readIndex:%d currentReadIndex:%d m_maximumReadIndex:%dn",m_readIndex,currentReadIndex,m_maximumReadIndex);
AtomicSub(&m_count, 1); // 真正讀取到了數(shù)據(jù),元素-1
return true;
}
} while (true);

assert(0);
// Add this return statement to avoid compiler warnings
return false;
},>

2.2.4 計(jì)算隊(duì)列的大小size函數(shù)的ABA問題與解決方案

template
QUEUE_INT ArrayLockFreeQueue::size() {
QUEUE_INT currentWriteIndex = m_writeIndex;
QUEUE_INT currentReadIndex = m_readIndex;

if (currentWriteIndex >= currentReadIndex)
return currentWriteIndex - currentReadIndex;
else
return Q_SIZE + currentWriteIndex - currentReadIndex;
},>

下面的場景描述了 size 為何會(huì)返回一個(gè)不正確的值:

1. 當(dāng) currentWriteIndex = m_writeIndex 執(zhí)行之后,m_writeIndex=3,m_readIndex = 2 那么實(shí)際 size 是 1;
2. 之后操作線程被搶占,且在它停止運(yùn)行的這段時(shí)間內(nèi),有 2 個(gè)元素被插入和從隊(duì)列中移除。所以 m_writeIndex=5,m_readIndex = 4,而 size 還是 1;
3. 現(xiàn)在被搶占的線程恢復(fù)執(zhí)行,讀取 m_readIndex 值,這個(gè)時(shí)候 currentReadIndex=4,currentWriteIndex=3;
4. currentReadIndex > currentWriteIndex'所以 m_totalSize + currentWriteIndex - currentReadIndex`被返回,這個(gè)值意味著隊(duì)列幾乎是滿的,而實(shí)際上隊(duì)列幾乎是空的。

解決方案:添加一個(gè)用于保存隊(duì)列中元素?cái)?shù)量的成員 count.這個(gè)成員可以通過 AtomicAdd/AtomicSub 來實(shí)現(xiàn)原子的遞增和遞減。

但需要注意的是這增加了一定開銷,因?yàn)樵舆f增,遞減操作比較昂貴也很難被編譯器優(yōu)化。如果可以容忍size函數(shù)的ABA問題,則可以不用count與AtomicAdd/AtomicSub。使用者可以根據(jù)自己的使用場合選擇是否承受額外的運(yùn)行時(shí)開銷。

2.2.5 與智能指針一起使用,內(nèi)存無法得到釋放

如果你打算用這個(gè)隊(duì)列來存放智能指針對(duì)象.需要注意,將一個(gè)智能指針存入隊(duì)列之后,如果它所占用的位置沒有被另一個(gè)智能指針覆蓋,那么它所指向的內(nèi)存是無法被釋放的(因?yàn)樗囊糜?jì)數(shù)器無法下降為 0).這對(duì)于一個(gè)操作頻繁的隊(duì)列來說沒有什么問題,但是程序員需要注意的是,一旦隊(duì)列被填滿過一次那么應(yīng)用程序所占用的內(nèi)存就不會(huì)下降,即使隊(duì)列被清空.除非自己做改動(dòng),每次 pop 手動(dòng) delete。

3. 多個(gè)?產(chǎn)者線程的情況下yielding處理器的必要性

讀者可能注意到了enqueue函數(shù)中使?了sched_yield()來主動(dòng)的讓出處理器,對(duì)于?個(gè)聲稱?鎖的算法??,這個(gè)調(diào)?看起來有點(diǎn)奇怪。正如?章開始的部分解釋過的,多線程環(huán)境下影響性能的其中?個(gè)因素就是Cache損壞。?產(chǎn)?Cache損壞的?種情況就是?個(gè)線程被搶占,操作系統(tǒng)需要保存被搶占線程的上下?,然后將被選中作為下?個(gè)調(diào)度線程的上下?載?。此時(shí)Cache中緩存的數(shù)據(jù)都會(huì)失效,因?yàn)樗潜粨屨季€程的數(shù)據(jù)?不是新線程的數(shù)據(jù)。

所以,當(dāng)此算法調(diào)?sched_yield()意味著告訴操作系統(tǒng):“我要把處理器時(shí)間讓給其它線程,因?yàn)槲乙却臣虑榈陌l(fā)?”。?鎖算法和通過阻塞機(jī)制同步的算法的?個(gè)主要區(qū)別在于?鎖算法不會(huì)阻塞在線程同步上,那么為什么在這?我們要主動(dòng)請(qǐng)求操作系統(tǒng)搶占??呢?這個(gè)問題的答案沒那么簡單。它與有多少個(gè)?產(chǎn)者線程在并發(fā)的往隊(duì)列中存放數(shù)據(jù)有關(guān):每個(gè)?產(chǎn)者線程所執(zhí)?的CAS操作都必須嚴(yán)格遵循FIFO次序,?個(gè)?于申請(qǐng)空間,另?個(gè)?于通知消費(fèi)者數(shù)據(jù)已經(jīng)寫?完成可以被讀取了。

如果我們的應(yīng)?程序只有唯?的?產(chǎn)者操作這個(gè)隊(duì)列,sche_yield()將永遠(yuǎn)沒有機(jī)會(huì)被調(diào)?,第2個(gè)CAS操作永遠(yuǎn)不會(huì)失敗。因?yàn)樵?個(gè)?產(chǎn)者的情況下沒有?能破壞?產(chǎn)者執(zhí)?這兩個(gè)CAS操作的FIFO順序。

?當(dāng)多于?個(gè)?產(chǎn)者線程往隊(duì)列中存放數(shù)據(jù)的時(shí)候,問題就出現(xiàn)了。概括來說,?個(gè)?產(chǎn)者通過第1個(gè)CAS操作申請(qǐng)空間,然后將數(shù)據(jù)寫?到申請(qǐng)到的空間中,然后執(zhí)?第2個(gè)CAS操作通知消費(fèi)者數(shù)據(jù)準(zhǔn)備完畢可供讀取了。這第2個(gè)CAS操作必須遵循FIFO順序,也就是說,如果A線程第?先執(zhí)?完第?個(gè)CAS操作,那么它也要第1個(gè)執(zhí)?完第2個(gè)CAS操作,如果A線程在執(zhí)?完第?個(gè)CAS操作之后停?,然后B線程執(zhí)?完第1個(gè)CAS操作,那么B線程將?法完成第2個(gè)CAS操作,因?yàn)樗却鼳先完成第2個(gè)CAS操作。?這就是問題產(chǎn)?的根源。讓我們考慮如下場景,3個(gè)消費(fèi)者線程和1個(gè)消費(fèi)者線程:

  • 線程1,2,3按順序調(diào)?第1個(gè)CAS操作申請(qǐng)了空間。那么它們完成第2個(gè)CAS操作的順序也應(yīng)該與這個(gè)順序?致,1,2,3。
  • 線程2?先嘗試執(zhí)?第2個(gè)CAS,但它會(huì)失敗,因?yàn)榫€程1還沒完成它的第2此CAS操作呢。同樣對(duì)于線程3也是?樣的。
  • 線程2和3將會(huì)不斷的調(diào)?它們的第2個(gè)CAS操作,直到線程1完成它的第2個(gè)CAS操作為?。
  • 線程1最終完成了它的第2個(gè)CAS,現(xiàn)在線程3必須等線程2先完成它的第2個(gè)CAS。
  • 線程2也完成了,最終線程3也完成。

在上?的場景中,?產(chǎn)者可能會(huì)在第2個(gè)CAS操作上?旋?段時(shí)間,?于等待先于它執(zhí)?第1個(gè)CAS操作的線程完成它的第2次CAS操作。在?個(gè)物理處理器數(shù)量?于操作隊(duì)列線程數(shù)量的系統(tǒng)上,這不會(huì)有太嚴(yán)重的問題:因?yàn)槊總€(gè)線程都可以分配在??的處理器上執(zhí)?,它們最終都會(huì)很快完成各?的第2次CAS操作。雖然算法導(dǎo)致線程處理忙等狀態(tài),但這正是我們所期望的,因?yàn)檫@使得操作更快的完成。也就是說在這種情況下我們是不需要sche_yield()的,它完全可以從代碼中刪除。

但是,在?個(gè)物理處理器數(shù)量少于線程數(shù)量的系統(tǒng)上,sche_yield()就變得?關(guān)重要了。讓我們?cè)俅慰疾樯?3個(gè)線程的場景,當(dāng)線程3準(zhǔn)備向隊(duì)列中插?數(shù)據(jù):如果線程1在執(zhí)?完第1個(gè)CAS操作,在執(zhí)?第2個(gè)CAS操作之前被搶占,那么線程2,3就會(huì)?直在它們的第2個(gè)CAS操作上忙等(它們忙等,不讓出處理器,線程1也就沒機(jī)會(huì)執(zhí)?,它們就只能繼續(xù)忙等,也就是說,如果不適用 sched_yield,一直自旋,那么可能多個(gè)線程同時(shí)阻塞在第二個(gè) CAS 那兒),直到線程1重新被喚醒,完成它的第2個(gè)CAS操作。這就是需要sche_yield()的場合了,操作系統(tǒng)應(yīng)該避免讓線程2,3處于忙等狀態(tài)。它們應(yīng)該盡快的讓出處理器讓線程1執(zhí)?,使得線程1可以把它的第2個(gè)CAS操作完成。這樣線程2和3才能繼續(xù)完成它們的操作。

4. 循環(huán)數(shù)組無鎖隊(duì)列的性能測試

4.1 性能測試

互斥鎖隊(duì)列 vs 互斥鎖+條件變量隊(duì)列 vs 內(nèi)存屏障鏈表 vs RingBuffer CAS 實(shí)現(xiàn)。

4寫1讀:性能中等

圖片

4寫4讀:性能中上

圖片

1寫4讀:性能最好

圖片

7寫7讀:性能比互斥鎖隊(duì)列還差

圖片

4.2 分析

雖然沒有分析第三個(gè)內(nèi)存屏障鏈表的代碼,但是我們不難看出互斥鎖+條件變量 與 內(nèi)存屏障鏈表 的性能差別不大。為什么呢?鏈表的方式需要不斷的申請(qǐng)和釋放元素。當(dāng)然,用內(nèi)存池可以適當(dāng)改善這個(gè)影響,但是內(nèi)存池在分配內(nèi)存與釋放內(nèi)存的時(shí)候也會(huì)涉及到線程間的數(shù)據(jù)競爭,所以用鏈表的方式性能相對(duì)提升不多。

隨著生產(chǎn)者數(shù)量的增加,無鎖隊(duì)列的效率迅速下降。因?yàn)樵诙鄠€(gè)生產(chǎn)者的情況下,第 2 個(gè) CAS 將對(duì)性能產(chǎn)生影響。我們通過測試得出循環(huán)數(shù)組的?鎖隊(duì)列在1寫4讀的場景下性能提升是最高的,因?yàn)橹挥幸粋€(gè)生產(chǎn)者,那么第二個(gè)CAS不會(huì)有yield的情況出現(xiàn)。由此我們可以得出一個(gè)結(jié)論,在一寫多讀的場景,我們可以優(yōu)先使用循環(huán)數(shù)組的?鎖隊(duì)列,比如下圖的場景。

圖片

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 接口
    +關(guān)注

    關(guān)注

    33

    文章

    9005

    瀏覽量

    153783
  • 封裝
    +關(guān)注

    關(guān)注

    128

    文章

    8694

    瀏覽量

    145557
  • 內(nèi)存
    +關(guān)注

    關(guān)注

    8

    文章

    3125

    瀏覽量

    75293
  • 線程安全
    +關(guān)注

    關(guān)注

    0

    文章

    13

    瀏覽量

    2555
收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評(píng)論

    相關(guān)推薦
    熱點(diǎn)推薦

    Linux下線程間通訊---讀寫和條件變量

    讀寫,它把對(duì)共享資源的訪問者劃分成讀者和者,讀者只對(duì)共享資源進(jìn)行訪問,者則需要對(duì)共享資源進(jìn)行操作。件變量是
    的頭像 發(fā)表于 08-26 20:44 ?1868次閱讀
    Linux下<b class='flag-5'>線程</b>間通訊---讀寫<b class='flag-5'>鎖</b>和條件變量

    探索字節(jié)隊(duì)列的魔法:類型支持、函數(shù)重載與線程安全

    探索字節(jié)隊(duì)列的魔法:類型支持、函數(shù)重載與線程安全代碼難度指數(shù):文章學(xué)習(xí)重點(diǎn):參數(shù)宏的使用技巧、引言在嵌入式系統(tǒng)和實(shí)時(shí)應(yīng)用中,數(shù)據(jù)的傳輸和
    的頭像 發(fā)表于 11-15 01:08 ?1246次閱讀
    探索字節(jié)<b class='flag-5'>隊(duì)列</b>的魔法:<b class='flag-5'>多</b>類型支持、函數(shù)重載與<b class='flag-5'>線程</b><b class='flag-5'>安全</b>

    隊(duì)列WHILE循環(huán)程序框架

    在需要多個(gè)隊(duì)列WHILE循環(huán)的程序框架里,將隊(duì)列捆綁提高的程序的可讀性
    發(fā)表于 09-27 22:13

    MCU上的原子操作

    變量的操作(必須單核),大家可以仔細(xì)想想。隊(duì)列是MCU上經(jīng)常用的,對(duì)中斷通信接口的緩沖非常方便可靠。以此為基礎(chǔ),可跨平臺(tái)
    發(fā)表于 03-06 09:39

    Linux下的線程安全是什么

    數(shù)據(jù)二義性。同步與互斥:同步:通過條件判斷,實(shí)現(xiàn)對(duì)靈界資源訪問的時(shí)序合理性?;コ猓和ㄟ^唯訪問,實(shí)現(xiàn)對(duì)臨界資源的安全性。、互斥
    發(fā)表于 07-01 13:34

    在MCU開發(fā)中使用多線程操作一寫一讀是否需要保護(hù)?

    在MCU(以常見的stm32為例)開發(fā)中使用多線程操作,我們經(jīng)常遇到的問題是關(guān)于多線程訪問數(shù)據(jù)的問題,多線程訪問數(shù)據(jù)基本上可以分為幾大類:
    發(fā)表于 02-01 15:42

    緩存如何實(shí)現(xiàn)

    少的業(yè)務(wù)場景:大部分請(qǐng)求是對(duì)數(shù)據(jù)進(jìn)行修改,少部分請(qǐng)求對(duì)數(shù)據(jù)進(jìn)行讀取。
    發(fā)表于 08-18 10:55 ?939次閱讀
    <b class='flag-5'>無</b><b class='flag-5'>鎖</b>緩存如何<b class='flag-5'>實(shí)現(xiàn)</b>

    利用CAS技術(shù)實(shí)現(xiàn)隊(duì)列

    【 導(dǎo)讀 】:本文 主要講解利用CAS技術(shù)實(shí)現(xiàn)隊(duì)列。 關(guān)于
    的頭像 發(fā)表于 01-11 10:52 ?2553次閱讀
    利用CAS技術(shù)<b class='flag-5'>實(shí)現(xiàn)</b><b class='flag-5'>無</b><b class='flag-5'>鎖</b><b class='flag-5'>隊(duì)列</b>

    cubeMX+STM32+Freertos 隊(duì)列時(shí)阻塞

    隊(duì)列時(shí)阻塞本例內(nèi)容是創(chuàng)建個(gè)隊(duì)列,由多個(gè)任務(wù)往隊(duì)列
    發(fā)表于 12-09 15:21 ?10次下載
    cubeMX+STM32+Freertos <b class='flag-5'>讀</b><b class='flag-5'>隊(duì)列</b>時(shí)阻塞

    關(guān)于CAS等原子操作介紹 隊(duì)列的鏈表實(shí)現(xiàn)方法

    在開始說隊(duì)列之前,我們需要知道個(gè)很重要的技術(shù)就是CAS操作——Compare & Set,或是 Compare & Swap,現(xiàn)在幾乎
    的頭像 發(fā)表于 05-18 09:12 ?3829次閱讀
    關(guān)于CAS等原子操作介紹 <b class='flag-5'>無</b><b class='flag-5'>鎖</b><b class='flag-5'>隊(duì)列</b>的鏈表<b class='flag-5'>實(shí)現(xiàn)</b>方法

    發(fā)燒友實(shí)測 | i.MX8MP 編譯DPDK源碼實(shí)現(xiàn)rte_ring環(huán)隊(duì)列進(jìn)程間通信

    作者|donatello1996來源|電子發(fā)燒友題圖|飛凌嵌入式rte_ring是個(gè)用CAS實(shí)現(xiàn)FIFO環(huán)形
    的頭像 發(fā)表于 01-10 16:29 ?2783次閱讀
    發(fā)燒友實(shí)測 | i.MX8MP 編譯DPDK源碼<b class='flag-5'>實(shí)現(xiàn)</b>rte_ring<b class='flag-5'>無</b><b class='flag-5'>鎖</b>環(huán)<b class='flag-5'>隊(duì)列</b>進(jìn)程間通信

    讀寫實(shí)現(xiàn)原理規(guī)則

    讀寫 互斥或自旋要么是加鎖狀態(tài)、要么是不加鎖狀態(tài),而且次只有個(gè)
    的頭像 發(fā)表于 07-21 11:21 ?1248次閱讀
    讀寫<b class='flag-5'>鎖</b>的<b class='flag-5'>實(shí)現(xiàn)</b>原理規(guī)則

    隊(duì)列的潛在優(yōu)勢

    隊(duì)列 先大致介紹隊(duì)列
    的頭像 發(fā)表于 11-09 09:23 ?876次閱讀
    <b class='flag-5'>無</b><b class='flag-5'>鎖</b><b class='flag-5'>隊(duì)列</b>的潛在優(yōu)勢

    c++線程的基本類型和用法

    。 互斥(Mutex) 互斥用于控制多個(gè)線程對(duì)他們之間共享資源互斥訪問的個(gè)信號(hào)量。也就是說是為了避免多個(gè)
    的頭像 發(fā)表于 11-09 15:02 ?3354次閱讀
    c++<b class='flag-5'>線程</b>中<b class='flag-5'>鎖</b>的基本類型和用法

    隊(duì)列解決的問題

    訪問;CPU訪問cache的速度要比訪問內(nèi)存要快的;由于線程頻繁切換,會(huì)造成cache失效,將導(dǎo)致應(yīng)用程序性能下降。 阻塞引起的CPU浪費(fèi) mutex是阻塞的,在個(gè)負(fù)載較重的應(yīng)用程
    的頭像 發(fā)表于 11-10 15:33 ?1359次閱讀
    <b class='flag-5'>無</b><b class='flag-5'>鎖</b><b class='flag-5'>隊(duì)列</b>解決的問題