我們在開發的過程中,經常遇到多個線程訪問同一資源的情況,也就是所謂的臨界資源。同一個資源對多個線程同時可見,如果均只是讀訪問,那沒毛病,關鍵是實際生產中讀寫是一起的。那麼問題來了,一個線程在寫數據,另外一個線程在讀數據,那麼讀線程讀取的很可能是亂碼,即使運氣好不是亂碼,也不是想要的數據,讀取的都是髒數據(這裏盜用一下數據庫術語)。更糟糕的是,多個線程同時在寫數據,你想象一下,那將是多麼混亂,數據將是一鍋粥。說到這裏,那麼線程間同步的方式有哪些呢?互斥量(Mutex)、條件變量(Condition)、時間(Event)、臨界區(CriticalSection)、信號量(Semphore)等等,這裏博主只是列舉出來一些常見的方法。
本篇博文我們將一起探討條件變量(condition),condition統稱和mutex一起使用,避免多個線程同時訪問condition,進而出現混亂。首先,我們一起來認識幾個常用的API吧。
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
int pthread_cond_destroy(pthread_cond_t *cond);
條件變量和互斥鎖一樣,都有靜態動態兩種創建方式,靜態方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
動態方式調用pthread_cond_init()函數儘管POSIX標準中爲條件變量定義了屬性,但在LinuxThreads中沒有實現,因此cond_attr值通常爲NULL,且被忽略。註銷一個條件變量需要調用pthread_cond_destroy(),只有在沒有線程在該條件變量上等待的時候才能註銷這個條件變量,否則返回EBUSY。因爲Linux實現的條件變量沒有分配什麼資源,所以註銷動作只包括檢查是否有等待線程。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex, const struct timespec *abstime);
pthread_cond_wait總和一個互斥鎖結合使用。在調用pthread_cond_wait前要先獲取鎖。pthread_cond_wait函數執行時先自動釋放指定的鎖(讓其他線程可以獲得鎖,進而可以是襲擊獲得條件變量,避免死鎖),然後等待條件變量的變化(由pthread_cond_signal和pthread_cond_broadcast函數喚醒,即獲得條件變量)。在函數調用返回之前,自動將指定的互斥量重新鎖住(在沒有獲得鎖之前函數阻塞)。pthread_cond_timedwait函數和pthread_cond_wait類似,pthread_cond_wait在獲得條件變量之前會一直阻塞在那裏,即永久等待。然而,pthread_cond_timedwait則靈活一點,允許設置等待時間,等待時間過後還沒有獲得條件變量,函數將不再等待。當然,最後的mutex還是需要等待的,獲得mutex後函數立即返回,返回錯誤碼ETIMEDOUT。
intpthread_cond_signal(pthread_cond_t * cond);
pthread_cond_broadcast(pthread_cond_t* cond);
pthread_cond_signal通過條件變量cond發送消息,若多個消息在等待,它只喚醒一個。pthread_cond_broadcast 可以喚醒所有。調用pthread_cond_signal後要立刻釋放互斥鎖,因爲pthread_cond_wait的最後一步是要將指定的互斥量重新鎖住,如果pthread_cond_signal之後沒有釋放互斥鎖,pthread_cond_wait仍然要阻塞。
互斥量(也稱爲互斥鎖)出自POSIX線程標準,可以用來同步同一進程中的各個線程。當然如果一個互斥量存放在多個進程共享的某個內存區中,那麼還可以通過互斥量來進行進程間的同步。互斥量,從字面上就可以知道是相互排斥的意思,它是最基本的同步工具,用於保護臨界區(共享資源),以保證在任何時刻只有一個線程能夠訪問共享的資源。
int pthread_mutex_destroy(pthread_mutex_t*mutex);
int pthread_mutex_init(pthread_mutex_t*restrict mutex,const pthread_mutexattr_t *restrict attr);
上面兩個函數分別由於互斥量的初始化和銷燬。如果互斥量是靜態分配的,可以通過常量進行初始化:
pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
當然也可以通過pthread_mutex_init()進行初始化。對於動態分配的互斥量由於不能直接賦值進行初始化就只能採用這種方式進行初始化,pthread_mutex_init()的第二個參數是互斥量的屬性,如果採用默認的屬性設置,可以傳入NULL。當不在需要使用互斥量時,需要調用pthread_mutex_destroy()銷燬互斥量所佔用的資源。
由於本篇博文主要將條件變量(condition的使用),關於mutex的更詳盡學習,博主後期會單獨寫一篇博文,還要牽扯到mutex的屬性設置相關函數。關於mutex的函數就介紹到這裏,下面我們看個程序實例吧。相信大家在學習操作系統的時候,都有接觸到生產者消費者模型、郵箱模型吧。博主這裏寫了一個測試程序,就是典型的生產者消費者模型。好了,不多說了,直接上代碼吧。
程序實例
/*
*
*使用pthread_cond_t和pthread_mutex_t實現一個多線程下的生產者消費者模型
*
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <queue>
#include <sys/time.h> //
#include <signal.h>
#include <errno.h>
#define PROCEDURE 50//生成線程數
#define CONSUMER 30//消費線程數
//打印指定錯誤號出錯信息
#define handle_error_no(no, msg)\
do {errno = no; perror(msg); exit(EXIT_FAILURE);}while(0)
//打印錯誤信息
#define handle_error(msg)\
do {perror(msg); exit(EXIT_FAILURE);}while(0);
//釋放mutex鎖,並退出線程
//將退出信息通過pthread_exit函數傳遞給主線程(主線程使用pthread_join的第二個參數接收)
#define thread_exit_id(msg, id, mutex)\
do {char *buf=(char*)malloc(256);sprintf(buf, "%d號%s退出\n",id, msg);pthread_mutex_unlock(&mutex);pthread_exit(buf);}while(0)
//退出線程
#define thread_pro_exit(msg, id)\
do {char *buf=(char*)malloc(256);sprintf(buf, "%d號%s退出\n",id, msg);pthread_exit(buf);}while(0)
pthread_cond_t cond; //定義條件變量
pthread_mutex_t mutex; //定義mutex(使訪問產品隊列和修改產品編號操作時原子的)
pthread_mutex_t mutex_shift;//控制開關鎖(此鎖在同一時間只有兩個線程在爭用,因爲此鎖只存在於生產線程的mutex鎖內)
bool shift = true; //生產按鈕,初始狀態爲開始狀態
//產品結構體
typedef struct produce
{
int pro_num;//生產產品號
}produce_t;
std::queue<produce_t> pro_queue; //已生產產品隊列
int pro_num = 0; //生產的產品號(從1開始編號)
//中斷信號(【ctrl+c】)處理函數
//使生產線程結束生產
void inthandler(int signum)
{
//加鎖操作生產開關
pthread_mutex_lock(&mutex_shift);
shift = false;//將生產開關關閉
//fprintf(stdout, "stop now!\n");
pthread_mutex_unlock(&mutex_shift);
}
//生產線程回調函數
void * procedure(void *arg)
{
int id = *(int *)arg;//接收線程參數
free(arg);//釋放參數資源(在主線程中申請的堆上空間)
produce_t produces;//存放待生產產品
while (1)
{
pthread_mutex_lock(&mutex);//加鎖
//訪問生產開關前先加鎖
//當用戶發出中斷信號,此時只有信號處理函數和當前線程在爭用此鎖
pthread_mutex_lock(&mutex_shift);
if (!shift)
{
pthread_mutex_unlock(&mutex_shift);//退出前先釋放鎖
break;
}
pthread_mutex_unlock(&mutex_shift);
++pro_num;//產品號加1
fprintf(stdout, "%d生產線程開始生產第%d號產品\n", id, pro_num);
usleep(10);//等待10微妙
produces.pro_num = pro_num;
fprintf(stdout, "%d生產線程結束生產第%d號產品\n", id, pro_num);
pro_queue.push(produces);//將生產的產品加入已生產產品隊列
//pthread_cond_signal(&cond);//隨機喚醒一個等待線程
pthread_cond_broadcast(&cond);//喚醒所用等待線程
pthread_mutex_unlock(&mutex);//解鎖
sched_yield();//讓出CPU資源,防止此線程一直佔用資源
}
//退出線程,在退出前先釋放mutex鎖資源
thread_exit_id("生產線程", id, mutex);
}
//消費線程回調函數
void * consumer(void *arg)
{
int id = *(int *)arg;//接收線程參數
free(arg);//是否參數資源(在主線程中申請的堆上空間)
produce_t produces;//存放貸消費產品
struct timespec outtime;//設置超時時間用
struct timeval nowtime;//存放當前系統時間用
while (1)
{
pthread_mutex_lock(&mutex);//加鎖
//當沒有產品時,等待
if (pro_queue.empty())
{
fprintf(stdout, "%d號消費線程等待消費產品開始\n", id);
//等待生產線程的條件
//調用該函數是,首先釋放鎖,在函數調用結束時加鎖,使恢復到加鎖狀態
//pthread_cond_wait(&cond, &mutex);
gettimeofday(&nowtime, NULL);
outtime.tv_sec = nowtime.tv_sec + 1;
outtime.tv_nsec = nowtime.tv_usec * 1000;
int ret = pthread_cond_timedwait(&cond, &mutex, &outtime);
if (ETIMEDOUT == ret) thread_exit_id("消費線程",id, mutex);
fprintf(stdout, "%d號消費線程等待消費產品結束\n", id);
}
produces = pro_queue.front();//從已生產產品隊列中取出隊頭產品
fprintf(stdout, "%d號消費線程消費產品%d開始\n", id, produces.pro_num);
usleep(10);//休眠10微妙
fprintf(stdout, "%d號消費線程消費產品%d結束\n", id, produces.pro_num);
pro_queue.pop();//從隊列中刪除該產品
pthread_mutex_unlock(&mutex);//釋放鎖
sched_yield();//讓出CPU資源
}
}
int main()
{
pthread_t tid[PROCEDURE+CONSUMER];//定義線程id數組
int i = 0;//循環變量
pthread_cond_init(&cond, NULL);//初始化條件量
pthread_mutex_init(&mutex, NULL);//初始化mutex
pthread_mutex_init(&mutex_shift, NULL);//初始化mutex
//註冊中斷信號
if (SIG_ERR == signal(SIGINT, inthandler))handle_error("signal");
//創建生產線程
for (i = 0; i < PROCEDURE; ++i)
{
int *arg = (int *)malloc(sizeof(int));//爲線程參數申請堆上空間
if (NULL == arg) handle_error("malloc");
*arg = i+1;//記錄線程號
int ret = pthread_create(tid+i, NULL, procedure, arg);
if (0 != ret) handle_error_no(ret, "pthread_creat");
}
//創建消費線程
for (; i < PROCEDURE+CONSUMER; ++i)
{
int *arg = (int *)malloc(sizeof(int));//爲線程參數申請堆上空間
if (NULL == arg) handle_error("malloc");
*arg = i - PROCEDURE + 1;//記錄線程號
int ret = pthread_create(tid + i, NULL, consumer, arg);
if (0 != ret) handle_error_no(ret, "pthread_creat");
}
void *buf = NULL;
//主線程阻塞等待子線程退出
for (i = 0; i < PROCEDURE + CONSUMER; ++i)
{
//等待子線程退出,接收pthread_exit傳出的參數
pthread_join(tid[i], &buf);
fprintf(stdout, "%s", (char*)buf);
free((char*)buf);//釋放堆上空間
buf = NULL;
}
pthread_cond_destroy(&cond);//銷燬條件變量
pthread_mutex_destroy(&mutex);//銷燬mutex
pthread_mutex_destroy(&mutex_shift);//銷燬mutex
}
程序截圖:
程序運行的比較快,這裏博主只截取了部分運行結果。在上述程序中,有用到前面講到過得信號,作爲最後退出的切入點。博主在程序中添加了很詳盡的註釋,相信大家已經看明白了,恭喜你又獲得了一項新技能。由於實例比較簡單,註釋也是相當的詳盡,博主就不再對程序進行贅述了,小夥伴們,趕緊狂敲代碼測試吧。