同步是指按預定的先後次序進行運行,線程同步是指多個線程通過特定的機制(如互斥量,事件對象,臨界區)來控制線程之間的執行順序,也可以說是在線程之間通過同步建立起執行順序的關係,如果沒有同步,那線程之間將是無序的。
線程的同步方式有很多種,其核心思想都是:在訪問臨界區的時候只允許一個 (或一類) 線程運行。
RT-Thread實現了三種線程間同步方式,信號量(semaphore)、互斥量(mutex)、和事件集(event)。
信號量
信號量可以實現多個同類資源的多線程互斥和同步。
特性:
- 不支持所有權,所有線程都可以操作。
- 遞歸訪問可能造成死鎖。
- 二值信號量類似互斥量,可能產生優先級反轉。
使用場景:
- 線程同步。當持有信號量的線程完成它處理的工作時,釋放這個信號量,可以把等待在這個信號量上的線程喚醒,讓它執行下一部分工作。這類場合也可以看成把信號量用於工作完成標誌:持有信號量的線程完成它自己的工作,然後通知等待該信號量的線程繼續下一部分工作。
- 鎖。二值信號量,保護臨界區資源。
- 中斷與線程的同步。中斷服務例程需要通知線程進行相應的數據處理。中斷與線程間的互斥不能採用信號量(鎖)的方式,而應採用開關中斷的方式。
- 資源計數。例如生產者與消費者,生產者可以對信號量進行多次釋放,而後消費者被調度到時能夠一次處理多個信號量資源。
信號量控制塊
信號量的值對應了信號量對象的實例數目、資源數目,假如信號量值爲 5,則表示共有 5 個信號量實例(資源)可以被使用,當信號量實例數目爲零時,再申請該信號量的線程就會被掛起在該信號量的等待隊列上,等待可用的信號量實例(資源)。
struct rt_ipc_object{
struct rt_object parent;
rt_list_t suspend_thread; /* 被掛起線程 */
};
struct rt_semaphore
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint16_t value; /**< value of semaphore. */
};
typedef struct rt_semaphore *rt_sem_t;
信號量初始化
sem->parent.parent.flag
信號量標誌參數決定了當信號量不可用時,多個線程等待的排隊方式。當選擇 RT_IPC_FLAG_FIFO(先進先出)方式時,那麼等待線程隊列將按照先進先出的方式排隊,先進入的線程將先獲得等待的信號量;當選擇 RT_IPC_FLAG_PRIO(優先級等待)方式時,等待線程隊列將按照優先級進行排隊,優先級高的等待線程將先獲得等待的信號量。
rt_err_t rt_sem_init(rt_sem_t sem, const char *name, rt_uint32_t value, rt_uint8_t flag)
{
rt_object_init(&(sem->parent.parent), RT_Object_Class_Semaphore, name);
rt_ipc_object_init(&(sem->parent));
sem->value = value;
sem->parent.parent.flag = flag;
return RT_EOK;
}
獲取信號量
線程通過 rt_sem_take()
來獲取信號量資源實例。當信號量值大於0時,線程獲得信號量,同時該信號量值減1;當信號量值等於0時,表示資源不可用,線程通過time
參數立即返回或掛起。
釋放信號量
釋放信號量時,先判斷是否有掛起線程。有則喚起一個,信號量值不變化;無則信號量值加一。
工程文件
線程2釋放信號量,線程1獲取。
互斥量
互斥量只能用於單個資源的互斥訪問。一種特殊的二值信號量。
特性:
- 支持所有權,只有擁有該互斥量的線程才能操作。
- 支持遞歸訪問。
- 通過優先級繼承算法降低優先級反轉問題產生的影響。
使用場景:
- 線程多次持有互斥量的情況下。這樣可以避免同一線程多次遞歸持有而造成死鎖的問題。
- 可能會由於多線程同步而造成優先級翻轉的情況。
互斥量控制塊
struct rt_mutex
{
struct rt_ipc_object parent; /* 繼承自 ipc_object 類 */
rt_uint16_t value; /* 互斥量的值 */
rt_uint8_t original_priority; /* 持有線程的原始優先級 */
rt_uint8_t hold; /* 持有線程的持有次數 */
struct rt_thread *owner; /* 當前擁有互斥量的線程 */
};
typedef struct rt_mutex* rt_mutex_t;
互斥量初始化
rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag){
RT_ASSERT(mutex != RT_NULL);
rt_object_init(&mutex->parent.parent, RT_Object_Class_Mutex, name);
rt_ipc_object_init(&mutex->parent);
mutex->parent.parent.flag = flag;
mutex->value = 1;
mutex->original_priority = RT_THREAD_PRIORITY_MAX - 1;
mutex->hold = 0;
mutex->owner = RT_NULL;
return RT_EOK;
}
獲取互斥量
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time)
線程獲取了互斥量,那麼線程就有了對該互斥量的所有權,即某一個時刻一個互斥量只能被一個線程持有。
如果互斥量已經被當前線程控制,則持有數mutex.hold
加一。如果互斥量被其他線程佔有,則當前線程在互斥量上掛起等待,直到其他線程釋放互斥量或等待時間超時,同時,如果當前線程的優先級高於互斥量擁有者的優先級,會發生優先級繼承。
釋放互斥量
rt_err_t rt_mutex_release(rt_mutex_t mutex)
只有互斥量的擁有者才能釋放互斥量,持有數mutex.hold
減一。如果持有數變成零,則判斷是否有線程掛起,有則喚起。
工程文件
thread1和thread2對全局變量number進行加1操作,都執行200000次,有mutex保護時,結果正確,開銷大。
事件集
事件集用一個 32 位無符號整型變量來表示,變量的每一位代表一個事件。
事件集控制塊
struct rt_event
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint32_t set; /* 事件集合,每一 bit 表示 1 個事件,bit 位的值可以標記某事件是否發生 */
};
typedef struct rt_event *rt_event_t;
發送事件
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set)
發送事件時遍歷事件集的掛起線程,判斷是否有線程的事件激活要求與當前 event 對象事件標誌值匹配,如果有,則喚醒該線程。
接收事件
rt_err_t rt_event_recv(rt_event_t event, rt_uint32_t set, rt_uint8_t option, rt_int32_t timeout, rt_uint32_t *recved)
或者叫等待事件觸發。系統首先根據 set 參數和接收選項 option 來判斷它要接收的事件是否發生,如果已經發生,則根據參數 option 上是否設置有 RT_EVENT_FLAG_CLEAR 來決定是否重置事件的相應標誌位,然後返回。如果沒有發生,則把等待的 set 和 option 參數填入線程本身的結構中,然後把線程掛起在此事件上,直到其等待的事件滿足條件或等待時間超過指定的超時時間。