目錄
一、同步與互斥的概念
二、同步與互斥并不簡單
三、各類方法的對比
一、同步與互斥的概念
一句話理解同步與互斥:我等你用完廁所,我再用廁所。
什么叫同步?就是:哎哎哎,我正在用廁所,你等會。
什么叫互斥?就是:哎哎哎,我正在用廁所,你不能進來。
同步與互斥經常放在一起講,是因為它們之的關系很大,“互斥”操作可以使用“同步”來 實現。我“等”你用完廁所,我再用廁所。這不就是用“同步”來實現“互斥”嗎?
再舉一個例子。在團隊活動里,同事A先寫完報表,經理B才能拿去向領導匯報。
經理B必須等同事A完成報表,AB之間有依賴,B必須放慢腳步,被稱為同步。在團隊活動中,同 事A已經使用會議室了,經理B也想使用,即使經理B是領導,他也得等著,這就叫互斥。經 理B跟同事A說:你用完會議室就提醒我。這就是使用"同步"來實現"互斥"。
01 void 搶廁所(void)
02 {
03 if (有人在用) 我瞇一會;
04 用廁所;
05 喂,醒醒,有人要用廁所嗎;
06 }
假設有A、B兩人早起搶廁所,A先行一步占用了;B慢了一步,于是就瞇一會;當A用完后叫醒B,B也就愉快地上廁所了。
在這個過程中,A、B是互斥地訪問“廁所”,“廁所”被稱之為臨界資源。我們使用了“休眠-喚醒”的同步機制實現了“臨界資源”的“互斥訪問”。
同一時間只能有一個人使用的資源,被稱為臨界資源。比如任務A、B都要使用串口來打印,串口就是臨界資源。如果A、B同時使用串口,那么打印出來的信息就是A、B混雜, 無法分辨。所以使用串口時,應該是這樣:A用完,B再用;B用完,A再用。
二、同步與互斥并不簡單
在裸機程序里,可以使用一個全局變量或靜態變量實現互斥操作,比如要互斥地使用 LCD,可以使用如下代碼:
01 int LCD_PrintString(int x, int y, char *str)
02 {
03 static int bCanUse = 1;
04 if (bCanUse)
05 {
06 bCanUse = 0;
07 /* 使用LCD */
08 bCanUse = 1;
09 return 0;
10 }
11 return -1;
12 }
但是在 RTOS 里,使用上述代碼實現互斥操作時,大概率是沒問題的,但是無法確保萬無一失。
假設如下場景:有兩個任務 A、B 都想調用 LCD_PrintString,任務 A 執行到第 4 行代碼時發現 bCanUse 為 1,可以進入 if 語句塊,它還沒執行第 6 句指令就被切換出去了;然后任務 B 也調用 LCD_PrintString,任務 B 執行到第 4 行代碼時也發現 bCanUse 為 1,也可以進入 if 語句塊使用 LCD。在這種情況下,使用靜態變量并不能實現互斥操作。
上述例子中,是因為第 4、第 6 兩條指令被打斷了,那么如下改進:在函數入口處先然讓 bCanUse 減一。這能否實現萬無一失的互斥操作呢?
01 int LCD_PrintString(int x, int y, char *str)
02 {
03 static int bCanUse = 1;
04 bCanUse--;
05 if (bCanUse == 0)
06 {
07 /* 使用LCD */
08 bCanUse++;
09 return 0;
10 }
11 else
12 {
13 bCanUse++;
14 return -1;
15 }
16 }
把第 4 行的代碼使用匯編指令表示如下:
04.1 LDR R0, [bCanUse] ????// 讀取bCanUse的值,存入寄存器R0
04.2 DEC R0, #1 ????????????????// 把R0的值減一
04.3 STR R0, [bCanUse]? ? // 把R0寫入變量bCanUse
假設如下場景:有兩個任務 A、B 都想調用 LCD_PrintString,任務 A 執行到第 04.1 行代碼時讀到的 bCanUse 為 1,存入寄存器 R0 就被切換出去了;然后任務 B 也調用LCD_PrintString,任務 B 執行到第 4 行時發現 bCanUse 為 1 并把它減為 0,執行到第 5 行代碼時發現條件成立可以進入 if 語句塊使用 LCD,然后任務 B 也被切換出去了;現在任務 A 繼續運行第 04.2 行代碼時 R0 為 1,運行到第 04.3 行代碼時把 bCanUse 設置為 0,后續也能成功進入 if 的語句塊。在這種情況下,任務 A、B 都能使用 LCD。
上述方法不能保證萬無一失的原因在于:在判斷過程中,被打斷了。如果能保證這個過程不被打斷,就可以了:通過關閉中斷來實現。
示例 1 的代碼改進如下:在第 5~7 行前關閉中斷。
01 int LCD_PrintString(int x, int y, char *str)
02 {
03 static int bCanUse = 1;
04 disable_irq(); //關閉中斷
05 if (bCanUse)
06 {
07 bCanUse = 0;
08 enable_irq(); //開啟中斷
09 /* 使用LCD */
10 bCanUse = 1;
11 return 0;
12 }
13 enable_irq(); //開啟中斷
14 return -1;
15 }
示例 2 的代碼改進如下:在第 5 行前關閉中斷。
01 int LCD_PrintString(int x, int y, char *str)
02 {
03 static int bCanUse = 1;
04 disable_irq();
05 bCanUse--;
06 enable_irq();
07 if (bCanUse == 0)
08 {
09 /* 使用LCD */
10 bCanUse++;
11 return 0;
12 }
13 else
14 {
15 disable_irq();
16 bCanUse++;
17 enable_irq();
18 return -1;
19 }
20 }
關閉中斷的方法并不是萬無一失的:假設現在有任務A和任務B在執行以下函數,在A打印后過了1ms,B被調度,但B只是進行判斷,但會一直失敗,過了1msA被調度繼續打印,打印完后過了1ms輪到B,B也是繼續判斷然后一直失敗,這樣子導致的結果是B占用了cpu資源,與同步例子類似,那么此時解決方法是:將B執行時若A已經在打印了,將B設置為阻塞狀態,于是A繼續打印,等到A打印完畢,將B喚醒,B才能正常打印。
void CalTask(void *params)//計時函數
{uint32_t i = 0;time = system_get_ns();//程序執行到此的時間for(i=0;i<10000000;i++){sum += i;}Cal_end = 1;//計算標志位置1time = system_get_ns() - time;//先計算程序到此的時間 再減去之前的時間 得到for循環中的時間vTaskDelete(NULL);
}void LcdPrintTask(void *params)
{int len;while(1){/* 打印信息 */LCD_PrintString(0,0,"waitting");vTaskDelay(2000);//讓此任務進入阻塞狀態 等上面任務執行完畢在執行這里 讓下面的while死循環不會占用cpu資源 不調用此函數則不會進入阻塞 會一直死等 為同步例子while(Cal_end == 0);//這里是在等待上面的計數完畢 若不使用上一行的vTaskDelay函數 則會在這里死等 占用cpu資源 因為兩個任務是交叉執行 若沒有上一行的vTaskDelay函數 燒錄完發現時間為2s左右 由于兩個任務叫交叉執行 若將B任務刪除只執行A任務 則時間差不多為一半即1ms 那么此時就可以調用vTaskDelay函數讓此任務進入阻塞狀態 讓task1先執行 執行完畢計時標志位置1 同時vTaskDelay函數計時完畢后進入此時的死循環 此時計時標志位置1直接跳出循環 不會讓程序卡在這里占用cpu資源 就可以準確計算出程序執行時間if(flag){flag = 0;LCD_ClearLine(0,0);len = LCD_PrintString(0,0,"Sum:");//這里的返回值是打印任務的長度len += LCD_PrintHex(len,0,sum,1);//將打印名字處后打印":"的長度一起加上 得到的長度是打印完任務名字和":"后的長度LCD_ClearLine(0,2);len = LCD_PrintString(0,2,"Time(ms):");len += LCD_PrintSignedVal(len,2,time/1000000);flag = 1;}vTaskDelete(NULL);}
}/* FreeRTOS.c中創建任務 */
xTaskCreate(CalTask,"taskA",128,NULL,osPriorityNormal,NULL);
xTaskCreate(LcdPrintTask,"taskB",128,&Task2,osPriorityNormal,NULL);
同步例子(任務B沒有阻塞)
由結果可知,兩個任務程序是交叉進行的,在RTOS中,任務級相同的任務每隔1s交叉運行,所以任務A執行1s(開始計時)后切到任務B,而A沒執行完畢 flag 始終為 0,那么任務B就會死等(任務A沒執行完畢,仍在計時) flag 為1,當B執行完A繼續執行(繼續計時)后 flag 為1后進入任務B才能成功計時完畢。可見時間大約為2s,若只有程序A執行,那么時間大約為1s左右。
互斥例子(任務B阻塞)
任務A不斷計時,任務A執行完1s后任務B執行,而任務B中調用了阻塞函數,所以繼續輪到任務A執行,任務A執行完畢跳轉到任務B中B成功打印計時值。(這里的計數值主要來源于任務A,所以若任務A執行完畢,會停止計時,所以B中的阻塞延時(只是2s后延時)對于計時結果無影響,為了能夠更好準確的得到計時值)
三、各類方法的對比
能實現同步、互斥的內核方法有:任務通知(task notification)、隊列(queue)、事件組 (event group)、信號量(semaphoe)、互斥量(mutex)。
它們都有類似的操作方法:獲取/釋放、阻塞/喚醒、超時。比如:
- 任務 A 獲取資源,用完后任務 A 釋放資源
- 任務 A 獲取不到資源則阻塞,任務 B 釋放資源并把任務 A 喚醒
- 任務 A 獲取不到資源則阻塞,并定個鬧鐘;A 要么超時返回,要么在這段時間內因為任務 B 釋放資源而被喚醒。
通過對比的方法來區分:
- 能否傳信息?還是只能傳遞狀態?
- 為眾生(所有任務都可以使用)?只為你(只能指定任務使用)?
- 我生產,你們消費?
- 我上鎖,只能由我開鎖
內核對 象 | 生產 者 | 消費 者 | 數據/狀態 | 說明 |
隊列 | ALL | ALL | 數據:若干個數據 誰都可以往隊列里扔數據, 誰都可以從隊列里讀數據 | 用來傳遞數據, 發送者、接收者無限制, 一個數據只能喚醒一 個接收者 |
事件組 | ALL | ALL | 多個位:或、與 誰都可以設置(生產)多個 位, 誰都可以等待某個位、若 干個位 | 用來傳遞事件, 可以是 N 個事件, 發送者、接受者無限制, 可以喚醒多個接收 者:像廣播 |
信號量 | ALL | ALL | 數量:0~n 誰都可以增加一個數量, 誰都可消耗一個數量 | 用來維持資源的個數, 生產者、消費者無限 制, 1 個資源只能喚醒 1 個接收者 |
任務通知 | ALL | 只有我 | 數據、狀態都可以傳輸, 使用任務通知時, 必須指定接受者 | N 對 1 的關系: 發送者無限制, 接收者只能是這個任 務 |
互斥量 | A 上鎖 | 只能 A 開鎖 | 位:0、1 我上鎖:1 變為 0, 只能由我開鎖:0 變為 1 | 就像一個空廁所, 誰使用誰上鎖, 也只能由他開鎖 |
使用圖形對比如下:
- 隊列:
- 里面可以放任意數據,可以放多個數據
- 任務、ISR 都可以放入數據;任務、ISR 都可以從中讀出數據
- 事件組:
- 一個事件用一 bit 表示,1 表示事件發生了,0 表示事件沒發生
- 可以用來表示事件、事件的組合發生了,不能傳遞數據
- 有廣播效果:事件或事件的組合發生了,等待它的多個任務都會被喚醒
- 信號量:
- 核心是"計數值"
- 任務、ISR 釋放信號量時讓計數值加 1
- 任務、ISR 獲得信號量時,讓計數值減 1
- 任務通知:
- 核心是任務的 TCB 里的數值
- 會被覆蓋
- 發通知給誰?必須指定接收任務
- 只能由接收任務本身獲取該通知
- 互斥量:
- 數值只有 0 或 1
- 誰獲得互斥量,就必須由誰釋放同一個互斥量