探索字節隊列的魔法:多類型支持、函數重載與線程安全
代碼難度指數:
文章學習重點:參數宏的使用技巧
一、引言
在嵌入式系統和實時應用中,數據的傳輸和處理是至關重要的。字節隊列(Byte Queue)是一種重要的數據結構,它能夠高效地存儲和管理數據流。通過使用字節隊列,我們可以靈活地處理不同類型的數據、確保數據的完整性,并在多線程環境中安全地進行操作。本文將深入探討字節隊列的概念、作用及其實現中的多類型支持、函數重載與線程安全機制。
1.1 隊列的基本概念
隊列是一種先進先出(FIFO,First In First Out)的數據結構。數據通過“入隊”(enqueue)操作添加到隊列的尾部,并通過“出隊”(dequeue)操作從隊列的頭部移除。在嵌入式系統中,隊列常用于:
- 數據緩沖:在數據產生和消費速率不匹配的情況下,隊列可以暫存數據,平衡輸入和輸出之間的差異。
- 任務調度:任務或事件的管理可以通過隊列來實現,確保它們按照特定順序被處理。
- 通信:隊列可以在不同模塊或線程之間傳遞信息,從而實現模塊間的解耦和同步。
1.2 字節隊列的不足
盡管字節隊列在嵌入式系統中提供了基本的數據存儲與管理能力,但其在實際應用中也存在一些明顯的不足:
- 缺乏多類型支持:傳統的字節隊列往往只能處理單一類型的數據,例如,使用固定的字節數組存儲數據,導致不同數據類型之間缺乏靈活性。為了支持不同類型的數據,開發者通常需要創建多個隊列,從而增加了代碼的復雜性和維護成本。
- 沒有函數重載:在 C 語言中,函數重載是通過不同的函數名稱來實現的,缺乏類似 C++的靈活性。這使得在隊列操作中無法方便地處理不同數量和類型的參數,導致代碼冗長且不易維護。
- 線程安全機制不足:在多線程環境中,若多個線程同時訪問字節隊列而沒有適當的同步機制,可能會導致數據損壞或不一致。傳統的字節隊列實現往往沒有內置的線程安全支持,增加了并發編程的難度。
二、字節隊列的改進
2.1 多類型支持的實現原理
問題: C 語言中的數組或緩沖區往往只能存儲單一類型的數據。例如,你可以定義一個 uint8_t 數組來存儲字節數據,或者一個 int32_t 數組來存儲整型數據。然而,在嵌入式系統中,我們常常需要處理各種類型的數據——8 位、16 位、32 位的整數、浮點數等等。為了避免為每種類型單獨創建隊列,我們希望有一個靈活的隊列,可以自動支持多種數據類型。
解決方案: 我們使用 C 語言的宏來解決這個問題。通過宏,隊列可以自動根據傳入的數據類型來計算所需的存儲空間。核心思想是:我們不關心具體的數據類型,而是通過宏和類型推導,計算每個數據需要的字節數,并按照字節的形式將數據存入隊列中。
使用 typeof 推斷數據類型:
C 語言的 typeof 關鍵字可以根據表達式自動推斷出數據類型,并可以通過該類型確定數據的大小。在我們的實現中,隊列的操作宏會通過 sizeof 來獲取傳入數據的字節大小。
示例:
#defineenqueue(queue,data)enqueue_bytes(queue,&data,sizeof(typeof(data)))
在上述宏中:
- typeof(data) 會推斷出 data 的類型,然后通過 sizeof(typeof(data))確定該類型占用的字節數。
- 通過將數據的地址傳遞給底層的 enqueue_bytes 函數,我們可以統一將所有類型的數據作為字節流處理。
通過這種方式,我們的隊列可以支持任意類型的數據,比如 8 位字節、16 位整數、32 位浮點數,甚至自定義的數據結構,只要知道它們的大小即可。
2.2 函數重載的實現原理
問題: 在 C++等語言中,函數重載允許你定義多個同名的函數,但參數類型或數量不同。然而,C 語言并不原生支持函數重載。這意味著如果我們想實現同名函數,處理不同類型或數量的參數,就需要想出另一種方法。
解決方案: 我們可以通過 C 語言的宏來“模擬”函數重載。宏的靈活性使得我們可以根據不同的參數數量或類型,選擇不同的底層函數進行處理。結合__VAARGS_等可變參數宏的特性,我們可以輕松實現這種重載行為。
使用宏實現參數數量的重載: enqueue 可以根據傳遞的參數數量,調用不同的函數。我們使用__VAARGS_(可變參數)來處理不同數量的參數。
enqueue 宏的完整代碼如下:
#define__CONNECT3(__A,__B,__C)__A##__B##__C
#define__CONNECT2(__A,__B)__A##__B
#defineCONNECT3(__A,__B,__C)__CONNECT3(__A,__B,__C)
#defineCONNECT2(__A,__B)__CONNECT2(__A,__B)
#defineSAFE_NAME(__NAME)CONNECT3(__,__NAME,__LINE__)
#define__PLOOC_VA_NUM_ARGS_IMPL(_0,_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,_11,\
_12,_13,_14,_15,_16,__N,...)__N
#define__PLOOC_VA_NUM_ARGS(...)\
__PLOOC_VA_NUM_ARGS_IMPL(0,##__VA_ARGS__,16,15,14,13,12,11,10,9,\
8,7,6,5,4,3,2,1,0)
#define__ENQUEUE_0(__QUEUE,__VALUE)\
({typeof((__VALUE))SAFE_NAME(value)=__VALUE;\
enqueue_bytes((__QUEUE),&(SAFE_NAME(value)),(sizeof(__VALUE)));})
#define__ENQUEUE_1(__QUEUE,__ADDR,__ITEM_COUNT)\
enqueue_bytes((__QUEUE),(__ADDR),__ITEM_COUNT*(sizeof(typeof((__ADDR[0])))))
#define__ENQUEUE_2(__QUEUE,__ADDR,__TYPE,__ITEM_COUNT)\
enqueue_bytes((__QUEUE),(__ADDR),(__ITEM_COUNT*sizeof(__TYPE)))
#defineenqueue(__queue,__addr,...)\
CONNECT2(__ENQUEUE_,__PLOOC_VA_NUM_ARGS(__VA_ARGS__))\
(__queue,(__addr),##__VA_ARGS__)
工作原理:
通過以上代碼,enqueue宏會根據傳遞的參數數量,自動選擇不同的實現版本
- 傳遞的可變參數如果為 0,調用__ENQUEUE_0;
- 傳遞的可變參數如果為 1,調用__ENQUEUE_1;
- 傳遞的可變參數如果為 2,調用__ENQUEUE_2。
2.2.1 函數重載的秘密 ——“__PLOOC_VA_NUM_ARGS”宏的深度剖析
__PLOOC_VA_NUM_ARGS 宏的代碼如下:
#define__PLOOC_VA_NUM_ARGS_IMPL(_0,_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,_11,_12,\
_13,_14,_15,_16,__N,...)__N
#define__PLOOC_VA_NUM_ARGS(...)\
__PLOOC_VA_NUM_ARGS_IMPL(0,##__VA_ARGS__,16,15,14,13,12,11,10,9,\
8,7,6,5,4,3,2,1,0)
- __PLOOC_VA_NUM_ARGS 宏的作用是它可以告訴我們實際傳遞了多少個參數
這里,首先構造了一個特殊的參數宏,__PLOOC_VA_NUM_ARGS_IMPL():
- 在涉及"..."之前,它要用戶至少傳遞 18 個參數;
- 這個宏的返回值就是第十八個參數的內容;
- 多出來的部分會被"..."吸收掉,不會產生任何后果
__PLOOC_VA_NUM_ARGS 的巧妙在于,它把__VA_ARGS__放在了參數列表的最前面,并隨后傳遞了 "16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0" 這樣的序號:
當__VA_ARGS__里有 1 個參數時,“1”對應第十八個參數__N,所以返回值是 1
當__VA_ARGS__里有 2 個參數時,“2”對應第十八個參數__N,所以返回值是 2
...
當__VA_ARGS__里有 9 個參數時,"9"對應第十八個參數__N,所以返回值是 9
舉個例子:
__PLOOC_VA_NUM_ARGS(0x,D,E,A,D)
展開為:
__PLOOC_VA_NUM_ARGS_IMPL(0,0x,D,E,A,D,16,15,14,13,12,11,10,9,\
8,7,6,5,4,3,2,1,0)
__PLOOC_VA_NUM_ARGS 的返回值是 5,從左往右數,第十八個參數,正好是“5”。
- 宏連接符##的作用
#define__CONNECT2(__A,__B)__A##__B
#defineCONNECT2(__A,__B)__CONNECT2(__A,__B)
宏連接符 ## 的主要作用就是連接兩個字符串,我們在宏定義中可以使用 ## 來連接兩個字符。預處理器在預處理階段對宏展開時,會將## 兩邊的字符合并,并刪除 ## 這兩個字符。
使用宏連接符 ##要注意一下兩條結論:
- 第一條:任何使用到膠水運算“##”對形參進行粘合的參數宏,一定需要額外的再套一層
- 第二條:其余情況下,如果要用到膠水運算,一定要在內部借助參數宏來完成粘合過程
為了理解這一“結論”,我們不妨舉一個例子:比如定義一個用于自動關閉中斷并在完成指定操作后自動恢復原來狀態的宏:
#defineSAFE_ATOM_CODE(...)\
{\
uint32_twTemp=__disable_irq();\
__VA_ARGS__;\
__set_PRIMASK(wTemp);\
}
由于這里定義了一個變量 wTemp,而如果用戶插入的代碼中也使用了同名的變量,就會產生很多問題:輕則編譯錯誤(重復定義);重則出現局部變量 wTemp 強行取代了用戶自定義的靜態變量的情況,從而直接導致系統運行出現隨機性的故障(比如隨機性的中斷被關閉后不再恢復,或是原本應該被關閉的全局中斷處于打開狀態等等)。為了避免這一問題,我們往往會想自動給這個變量一個不會重復的名字,比如借助 __LINE__ 宏給這一變量加入一個后綴:
#defineSAFE_ATOM_CODE(...)\
{\
uint32_twTemp##__LINE__=__disable_irq();\
__VA_ARGS__;\
__set_PRIMASK(wTemp);\
}
假設這里 SAFE_ATOM_CODE 所在行的行號是 123,那么我們期待的代碼展開是這個樣子的(我重新縮進過了):
...
{
uint32_twTemp123=__disable_irq();
__VA_ARGS__;
__set_PRIMASK(wTemp);
}
...
然而,實際展開后的內容是這樣的:
...
{
uint32_twTemp__LINE__=__disable_irq();
__VA_ARGS__;
__set_PRIMASK(wTemp);
}
...
這里,__LINE__似乎并沒有被正確替換為 123,而是以原樣的形式與 wTemp 粘貼到了一起——這就是很多人經常抱怨的 __LINE__ 宏不穩定的問題。實際上,這是因為上述宏的構建沒有遵守前面所列舉的兩條結論導致的。
從內容上看,SAFE_ATOM_CODE() 要粘合的對象并不是形參,根據結論第二條,需要借助另外一個參數宏來幫忙完成這一過程。為此,我們需要引入一個專門的宏:
#defineCONNECT2(__A,__B)__A##__B
注意到,這個參數宏要對形參進行膠水運算,根據結論第一條,需要在宏的外面再套一層,因此,修改代碼得到:
#define__CONNECT2(__A,__B)__A##__B
#defineCONNECT2(__A,__B)__CONNECT2(__A,__B)
修改前面的定義得到:
#defineSAFE_ATOM_CODE(...)\
{\
uint32_tCONNECT2(wTemp,__LINE__)=\
__disable_irq();\
__VA_ARGS__;\
__set_PRIMASK(wTemp);\
}
- 對 enqueue 的封裝進行展開
#defineenqueue(__queue,__addr,...)\
CONNECT2(__ENQUEUE_,__PLOOC_VA_NUM_ARGS(__VA_ARGS__))\
(__queue,(__addr),##__VA_ARGS__)
CONNECT2 會根據__PLOOC_VA_NUM_ARGS 返回的數量,與__ENQUEUE_進行連接,
- __PLOOC_VA_NUM_ARGS 返回的數量如果為 0,調用__ENQUEUE_0(__queue,(__addr),##__VA_ARGS__);
- __PLOOC_VA_NUM_ARGS 返回的數量如果為 1,調用__ENQUEUE_1(__queue,(__addr),##__VA_ARGS__);
- __PLOOC_VA_NUM_ARGS 返回的數量如果為 2,調用__ENQUEUE_2(__queue,(__addr),##__VA_ARGS__);
舉個例子:
staticbyte_queue_tmy_queue;
uint8_tdata1=0XAA;
enqueue(&my_queue,data1);//__ENQUEUE_0(&my_queue,data1)
enqueue(&my_queue,&data1,1);//__ENQUEUE_1(&my_queue,&data1,1)
enqueue(&my_queue,&data1,uint8_t,1);//__ENQUEUE_2(&my_queue,&data1,uint8_t,1)
/*__ENQUEUE_0,__ENQUEUE_1,__ENQUEUE_2,展開后調用的都是同一個接口*/
enqueue_bytes(&my_queue,&data1,1)
2.3 線程安全的實現原理
問題: 在多線程環境下,如果多個線程同時對同一個隊列進行操作,可能會引發數據競爭問題,導致數據損壞或不一致。為了避免這種情況,我們需要保證每次對隊列的操作是原子的,即不可打斷的。
解決方案: 在嵌入式系統中,常用的方法是通過禁用中斷或使用鎖機制來保證數據的一致性。在我們的實現中,我們使用禁用中斷的方式來確保線程安全。這是一種非常常見的技術,尤其是在實時系統中。
為了盡量降低關中斷對實時性的影響,我們只對操作隊列指針的操作進行關中斷保護,相對耗時間的數據拷貝不進行關中斷。
函數偽代碼如下:
boolenqueue_bytes(...)
{
boolbEarlyReturn=false;
safe_atom_code(){
if(!this.bMutex){
this.bMutex=true;
}else{
bEarlyReturn=true;
}
}
if(bEarlyReturn){
returnfalse;
}
safe_atom_code(){
/*隊列指針操作*/
...
}
/*數據操作*/
memcpy(...);
...
this.bMutex=false;
returntrue;
}
原子宏 safe_atom_code()的實現:
前邊的例子中,我們實現了一個 SAFE_ATOM_CODE 的原子宏,唯一的問題是,這樣的寫法,在調試時完全沒法在用戶代碼處添加斷點(編譯器會認為宏內所有的內容都寫在了同一行),這是大多數人不喜歡使用宏來封裝代碼結構的最大原因。
接下來我們用另一種實現方式來解決這個問題,代碼如下:
#define__CONNECT3(__A,__B,__C)__A##__B##__C
#define__CONNECT2(__A,__B)__A##__B
#defineCONNECT3(__A,__B,__C)__CONNECT3(__A,__B,__C)
#defineCONNECT2(__A,__B)__CONNECT2(__A,__B)
#defineSAFE_NAME(__NAME)CONNECT3(__,__NAME,__LINE__)
#include"cmsis_compiler.h"
#definesafe_atom_code()\
for(uint32_tSAFE_NAME(temp)=\
({uint32_tSAFE_NAME(temp2)=__get_PRIMASK();\
__disable_irq();\
SAFE_NAME(temp2);}),*SAFE_NAME(temp3)=NULL;\
SAFE_NAME(temp3)++==NULL;\
__set_PRIMASK(SAFE_NAME(temp)))
#endif
工作原理:
safe_atom_code()通過一個循環結構確保在隊列操作期間,中斷被禁用。循環結束后自動恢復中斷。
2.3.1 for 循環的妙用
首先構造一個只執行一次的 for 循環結構:
for(inti=1;i>0;i--){
...
}
對于這樣的 for 循環結構,幾個關鍵部分就有了新的意義:
- 在執行用戶代碼之前(灰色部分),有能力進行一定的“準備工作”(Before 部分);
- 在執行用戶代碼之后,有能力執行一定的“收尾工作”(After 部分);
- 在 init_clause 階段有能力定義一個“僅僅只覆蓋” for 循環的,并且只對 User Code 可見的局部變量——換句話說,這些局部變量是不會污染 for 循環以外的地方的。
利用這樣的結構,我們很容易就能構造出一個可以通過花括號的形式來包裹用戶代碼的原子操作 safe_atom_code(),在執行用戶代碼之前關閉中斷,在執行完用戶代碼之后打開中斷,還不影響在用戶代碼中添加斷點,單步執行。
需要注意的是,如果需要中途退出循環,需要使用continue退出原子操作,不能使用break。
2.4 總結
通過上述的多類型支持、函數重載和線程安全的實現,我們大大增強了字節隊列的靈活性和實用性:
- 多類型支持: 自動推斷數據類型和大小,支持不同類型數據的隊列操作。
- 函數重載: 通過宏模擬 C 語言的函數重載,靈活處理不同數量和類型的參數。
- 線程安全: 通過禁用中斷機制確保隊列操作在多線程環境中的原子性,避免數據競爭問題。
這些改進使得我們的字節隊列不僅可以在單線程環境中高效運行,還能在復雜的多線程系統中保持數據的一致性與安全性。
三、API 接口
#definequeue_init(__queue,__buffer,__size,...)\
__PLOOC_EVAL(__QUEUE_INIT_,##__VA_ARGS__)\
(__queue,(__buffer),(__size),##__VA_ARGS__)
#definedequeue(__queue,__addr,...)\
__PLOOC_EVAL(__DEQUEUE_,##__VA_ARGS__)\
(__queue,(__addr),##__VA_ARGS__)
#defineenqueue(__queue,__addr,...)\
__PLOOC_EVAL(__ENQUEUE_,##__VA_ARGS__)\
(__queue,(__addr),##__VA_ARGS__)
#definepeek_queue(__queue,__addr,...)\
__PLOOC_EVAL(__PEEK_QUEUE_,##__VA_ARGS__)\
(__queue,(__addr),##__VA_ARGS__)
extern
byte_queue_t*queue_init_byte(byte_queue_t*ptObj,void*pBuffer,uint16_thwItemSize,boolbIsCover);
extern
boolreset_queue(byte_queue_t*ptObj);
extern
uint16_tenqueue_bytes(byte_queue_t*ptObj,void*pDate,uint16_thwDataLength);
extern
uint16_tdequeue_bytes(byte_queue_t*ptObj,void*pDate,uint16_thwDataLength);
extern
boolis_queue_empty(byte_queue_t*ptQueue);
extern
boolis_peek_empty(byte_queue_t*ptObj);
extern
uint16_tpeek_bytes_queue(byte_queue_t*ptObj,void*pDate,uint16_thwDataLength);
extern
voidreset_peek(byte_queue_t*ptQueue);
extern
voidget_all_peeked(byte_queue_t*ptQueue);
extern
uint16_tget_peek_status(byte_queue_t*ptQueue);
extern
voidrestore_peek_status(byte_queue_t*ptQueue,uint16_thwCount);
extern
uint16_tget_queue_count(byte_queue_t*ptObj);
extern
uint16_tget_queue_available_count(byte_queue_t*ptObj);
四、API 說明
- 初始化隊列
queue_init(__queue,__buffer,__size,...)
參數說明:
參數名 | 描述 |
---|---|
__QUEUE | 隊列的地址 |
__BUFFER | 隊列緩存的首地址 |
__BUFFER_SIZE | 隊列長度 |
可變參數 | 是否覆蓋,默認否 |
- 入隊
#defineenqueue(__queue,__addr,...)
參數說明:
參數名 | 描述 |
---|---|
__QUEUE | 隊列的地址 |
__ADDR | 待入隊的數據或者數據的地址 |
... | 可變參數,需要入隊的數據個數,或者數據類型和個數,如果為空,則只入隊一個數據 |
- 出隊
#definedequeue(__queue,__addr,...)
參數說明:
參數名 | 描述 |
---|---|
__QUEUE | 隊列的地址 |
__ADDR | 用于保存出隊數據變量的地址 |
... | 可變參數,需要出隊的數據個數,或者數據類型和個數,如果為空,則只出隊一個數據 |
- 查看
#definepeek_queue(__queue,__addr,...)
參數說明:
參數名 | 描述 |
---|---|
__QUEUE | 隊列的地址 |
__ADDR | 用于保存查看數據變量的地址 |
... | 可變參數,數據類型和需要查看的數據個數,如果為空,則只查看一個數據 |
五、快速使用
代碼開源地址:https://github.com/Aladdin-Wang/wl_queue
或者打開MicroBoot,介紹鏈接:徹底解決單片機BootLoader升級程序失敗問題,只勾選queue,如圖所示:
使用實例:
#include"ring_queue.h"
uint8_tdata1=0XAA;
uint16_tdata2=0X55AA;
uint32_tdata3=0X55AAAA55;
uint16_tdata4[]={0x1234,0x5678};
typedefstructdata_t{
uint32_ta;
uint32_tb;
uint32_tc;
}data_t;
data_tdata5={
.a=0X11223344,
.b=0X55667788,
.c=0X99AABBCC,
};
uint8_tdata[100];
staticuint8_ts_hwQueueBuffer[100];
staticbyte_queue_tmy_queue;
queue_init(&my_queue,s_hwQueueBuffer,sizeof(s_hwQueueBuffer));
//根據變量的類型,自動計算對象的大小
enqueue(&my_queue,data1);
enqueue(&my_queue,data2);
enqueue(&my_queue,data3);
//一下三種方式都可以正確存儲數組
enqueue(&my_queue,data4,2);//可以不指名數據類型
enqueue(&my_queue,data4,uint16_t,2);//也可以指名數據類型
enqueue(&my_queue,data4,uint8_t,sizeof(data4));//或者用其他類型
//一下兩種方式都可以正確存儲結構體類型
enqueue(&my_queue,data5);//根據結構體的類型,自動計算對象的大小
enqueue(&my_queue,&data5,uint8_t,sizeof(data5));//也可以以數組方式存儲
enqueue(&my_queue,(uint8_t)0X11);//常量默認為int型,需要強制轉換數據類型
enqueue(&my_queue,(uint16_t)0X2233);//常量默認為int型,需要強制轉換數據類型
enqueue(&my_queue,0X44556677);
enqueue(&my_queue,(char)'a');//單個字符也需要強制轉換數據類型
enqueue(&my_queue,"bc");//字符串默認會存儲空字符\0
enqueue(&my_queue,"def");
//讀出全部數據
dequeue(&my_queue,data,get_queue_count(&my_queue));
結語
本文的目的,告訴大家如何正確的看待宏——宏不是阻礙代碼開發和可讀性的魔鬼:
宏不是奇技淫巧
宏可以封裝出其它高級語言所提供的“基礎設施”
設計良好的宏可以提升代碼的可讀性,而不是破壞它
設計良好的宏并不會影響調試
宏可以用來固化某些模板,避免每次都重新編寫復雜的語法結構
-
嵌入式系統
+關注
關注
41文章
3593瀏覽量
129473 -
函數
+關注
關注
3文章
4331瀏覽量
62618 -
數據結構
+關注
關注
3文章
573瀏覽量
40130
發布評論請先 登錄
相關推薦
評論