多執行緒程式設計C語言版(附程式碼示例)
「來源: |嵌入式大雜燴 ID:zhengnian-2018」
本文探討PODIX執行緒相關內容。
執行緒的概念
什麼是多執行緒,提出這個問題的時候,我還是很老實的拿出作業系統的書,按著上面的話敲下“為了減少程序切換和建立開銷,提高執行效率和節省資源,我們引入了執行緒的概念,與程序相比較,執行緒是CPU排程的一個基本單位。”
當 Linux 最初開發時,在核心中並不能真正支援執行緒。那為什麼要使用多執行緒?
使用多執行緒的理由之一是和程序相比,它是一種非常“節儉”的多工操作方式。運行於一個程序中的多個執行緒,它們彼此之間使用相同的地址空間,共享大部分資料,啟動一個執行緒所花費的空間遠遠小於啟動一個程序所花費的空間,而且,執行緒間彼此切換所需的時間也遠遠小於程序間切換所需要的時間。
那麼執行緒是幹什麼的呢?簡要概括下執行緒的職責:執行緒是程式中完成一個獨立任務的完整執行序列。
執行緒的管理
建立執行緒
#include
intpthread_create(pthread_t *thread, constpthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- thread:執行緒id,唯一標識
- attr:執行緒屬性,引數可選
- start_routine:執行緒執行函式
- arg:傳遞給執行緒的引數
Demo1:建立一個執行緒
#include
#include
void *workThreadEntry(void *args)
{
char*str = (char*)args;
printf(“threadId:%lu,argv:%s\n”,pthread_self(),str);
}
intmain(int argc,char *agrv[])
{
pthread_t thread_id;
char*str = “hello world”;
pthread_create(&thread_id,NULL,workThreadEntry,str);
printf(“threadId=%lu\n”,pthread_self());
pthread_join(thread_id,NULL);
}
編譯執行
$ gcc -o main main。c -pthread
$ 。/main
threadId=140381594486592
threadId:140381585938176,argv:hello world
執行結果是建立一個執行緒,列印執行緒id和主執行緒傳遞過來的引數。
執行緒退出與等待
在Demo1中我們用到了pthread_join這個函式
#include
intpthread_join(pthread_t thread, void **retval);
這是一個阻塞函式,用於等待執行緒退出,對執行緒資源進行收回。
一個執行緒對應一個pthread_join()呼叫,對同一個執行緒進行多次pthread_join()呼叫屬於邏輯錯誤,俗稱耍流氓。
那麼執行緒什麼時候退出?
1。線上程函式執行完後,該執行緒也就退出了
2。執行緒內呼叫函式pthread_exit()主動退出
3。當執行緒可以被取消時,透過其他執行緒呼叫pthread_cancel的時候退出
4。建立執行緒的程序退出
5。主執行緒執行了exec類函式,該程序的所有的地址空間完全被新程式替換,子執行緒退出
執行緒的狀態
執行緒pthread有兩種狀態joinable狀態和unjoinable狀態,如果執行緒是joinable狀態,當執行緒函式自己返回退出時或pthread_exit時都不會釋放執行緒所佔用堆疊和執行緒描述符(總計8K多)。只有當你呼叫了pthread_join之後這些資源才會被釋放。若是unjoinable狀態的執行緒,這些資源線上程函式退出時或pthread_exit時自動會被釋放。pthread的狀態在建立執行緒的時候指定,建立一個執行緒預設的狀態是joinable。
狀態為joinable的執行緒可在建立後,用pthread_detach()顯式地分離,但分離後不可以再合併,該操作不可逆。
#include
intpthread_detach(pthread_t thread);
pthread_detach這個函式就是用來分離主執行緒和子執行緒,這樣做的好處就是當子執行緒退出時系統會自動釋放執行緒資源。
主執行緒與子執行緒分離,子執行緒結束後,資源自動回收。
執行緒取消
線上程的退出中我們說到執行緒可以被其他執行緒結束。
1。一個執行緒可以呼叫pthread_cancel來取消另一個執行緒。
2。被取消的執行緒需要被join來釋放資源。
3。被取消的執行緒的返回值為PTHREAD_CANCELED
有關執行緒的取消,一個執行緒可以為如下三個狀態:
1。可非同步取消:一個執行緒可以在任何時刻被取消。
2。可同步取消:取消的請求被放在佇列中,直到執行緒到達某個點,才被取消。
3。不可取消:取消的請求被忽略。
首先執行緒預設是可以取消的,透過pthread_setcancelstate設定執行緒的取消狀態屬性
#include
intpthread_setcancelstate(int state, int *oldstate);
intpthread_setcanceltype(int type, int *oldtype);
呼叫pthread_setcanceltype來設定執行緒取消的方式:
pthread_setcanceltype (PTHREAD_CANCEL_ASYNCHRONOUS, NULL); //非同步取消、
pthread_setcanceltype (PTHREAD_CANCEL_DEFERRED, NULL); //同步取消、
pthread_setcanceltype (PTHREAD_CANCEL_DISABLE, NULL); //不能取消
執行緒回收
Linux提供回收器(cleanup handler),它是一個API函式,線上程退出的時候被呼叫。
#include
voidpthread_cleanup_push(void (*routine)(void *),
void pthread_cleanup_pop(int execute);
這兩個API是為了解決執行緒終止或者異常終止時,釋放資源的問題。
Demo2:執行緒回收示例
//pthread_pop_push。c
#include
#include
#include
#include
voidcleanup()
{
printf(“cleanup\n”);
}
void *test_cancel(void)
{
//註冊一個回收器
pthread_cleanup_push(cleanup,NULL);
printf(“test_cancel\n”);
while(1)
{
printf(“test message\n”);
sleep(1);
}
//呼叫且登出回收器
pthread_cleanup_pop(1);
}
intmain()
{
pthread_t tid;
pthread_create(&tid,NULL,(void *)test_cancel,NULL);
sleep(2);
pthread_cancel(tid);
pthread_join(tid,NULL);
}
執行緒的私有資料
我們在開頭的概述中講到運行於一個程序中的多個執行緒,它們彼此之間使用相同的地址空間,共享大部分資料。既然是大部分資料那麼就有屬於執行緒的私有資料
TSD私有資料,同名但是不同記憶體地址的私有資料結構
建立私有資料
intpthread_key_create(pthread_key_t *__key,void (*__destr_function) (void *));
intpthread_key_delete(pthread_key_t __key);
- __key:pthread_key_t型別的變數
- __destr_function:清理函式,用來線上程釋放該執行緒儲存的時候被呼叫
建立和刪除私有資料是對應的
讀寫私有資料
externintpthread_setspecific(pthread_key_t __key,constvoid *__pointer);
void *pthread_getspecific(pthread_key_t __key);
- __key:pthread_key_t型別的變數
- __pointer:void*型別的值
Demo3:執行緒私有資料示例
//pthread_key_test。c
#include
#include
#include
pthread_key_t key;
voidechomsg(void *t)
{
printf(“destructor excuted in thread %lu,param=%p\n”,pthread_self(),((int *)t));
}
void * thread1(void *arg)
{
int i=10;
printf(“set key value %d in thread %lu\n”,i,pthread_self());
pthread_setspecific(key,&i);
printf(“thread 2s。。\n”);
sleep(2);
printf(“thread:%lu,key:%d,address:%p\n”,pthread_self(),*((int *)pthread_getspecific(key)),(int *)pthread_getspecific(key));
}
void * thread2(void *arg)
{
int temp=20;
printf(“set key value %d in thread %lu\n”,temp,pthread_self());
pthread_setspecific(key,&temp);
printf(“thread 1s。。\n”);
sleep(1);
printf(“thread:%lu,key:%d,address:%p\n”,pthread_self(),*((int *)pthread_getspecific(key)),(int *)pthread_getspecific(key));
}
intmain(void)
{
pthread_t tid1,tid2;
pthread_key_create(&key,echomsg);
pthread_create(&tid1,NULL,(void *)thread1,NULL);
pthread_create(&tid2,NULL,(void *)thread2,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_key_delete(key);
return0;
}
執行結果
$ 。/main
set key value 20 in thread 139739044730624
thread 1s。。
set key value 10 in thread 139739053123328
thread 2s。。
thread:139739044730624,key:20,address:0x7f17881f4ed4
destructor excuted in thread 139739044730624,param=0x7f17881f4ed4
thread:139739053123328,key:10,address:0x7f17889f5ed4
destructor excuted in thread 139739053123328,param=0x7f17889f5ed4
從結果集裡面可以看到key在兩個執行緒中的地址是一樣的但是key值不同。
執行緒屬性
在建立執行緒的時候,pthread_create第二個引數設為NULL即執行緒屬性,一般情況下,使用預設屬性就可以解決我們開發過程中的大多數問題。
執行緒屬性標識pthread_attr_t結構如下
//執行緒屬性結構如下:
typedefstruct
{
int detachstate; //執行緒的分離狀態
int schedpolicy; //執行緒排程策略
structsched_param schedparam; //執行緒的排程引數
int inheritsched; //執行緒的繼承性
int scope; //執行緒的作用域
size_t guardsize; //執行緒棧末尾的警戒緩衝區大小
int stackaddr_set; //執行緒的棧設定
void* stackaddr; //執行緒棧的位置
size_t stacksize; //執行緒棧的大小
}pthread_attr_t;
屬性值不能直接設定,須使用相關函式進行操作,初始化的函式為pthread_attr_init,這個函式必須在pthread_create函式之前呼叫。之後須用pthread_attr_destroy函式來釋放資源。
#include
intpthread_attr_init(pthread_attr_t *attr);
intpthread_attr_destroy(pthread_attr_t *attr);
執行緒屬性主要包括如下屬性:
作用域(scope)
棧尺寸(stack size)
棧地址(stack address)
優先順序(priority)
分離的狀態(detached state)
排程策略和引數(scheduling policy and parameters)。
預設的屬性為非繫結、非分離、預設1M的堆疊、與父程序同樣級別的優先順序。
這裡簡要說明下執行緒分離狀態(detached state)和堆疊大小(stacksize),主要是這個我個人用的比較多
Demo4:執行緒屬性設定
#include
#include
#include
void* thread_run(void* args){
size_t threadSize;
pthread_attr_t* threadAttr = (pthread_attr_t*)args;
pthread_attr_getstacksize(threadAttr,&threadSize);
printf(“thread threadSize:%ld\n”,threadSize);
}
intmain(){
pthread_t threadId;
pthread_attr_t threadAttr;
pthread_attr_init(&threadAttr);
pthread_attr_setdetachstate(&threadAttr, PTHREAD_CREATE_DETACHED); //PTHREAD_CREATE_DETACHED:執行緒分離 ;PTHREAD_CREATE_JOINABLE:非分離執行緒
pthread_attr_setstacksize(&threadAttr, 4 * 1024 * 1024);
pthread_create(&threadId, &threadAttr, thread_run, &threadAttr);
sleep(1);
return0;
}
執行結果
$ gcc -o main main。c -lpthread
$ 。/main
thread threadSize:4194304
這樣我們就建立一個堆疊大小為 4194304 執行緒分離的執行緒。
Linux執行緒屬性總結文章參考:
https://blog。csdn。net/nkguohao/article/details/38796475
執行緒的同步互斥
在開頭說道,在多執行緒的程式中,多個執行緒共享堆疊空間,那麼就會存在問題
互斥鎖
在多執行緒的程式中,多個執行緒共享臨界區資源,那麼就會有競爭問題,互斥鎖mutex是用來保護執行緒間共享的全域性變數安全的一種機制, 保證多執行緒中在某一時刻只允許某一個執行緒對臨界區的訪問。
POSIX標準下互斥鎖是pthread_mutex_t,與之相關的函式有:
intpthread_mutex_init(pthread_mutex_t * mutex , pthread_mutexattr_t * attr);
intpthread_mutex_destroy(pthread_mutex_t * mutex);
intpthread_mutex_lock(pthread_mutex_t * mutex ); //阻塞式
intpthread_mutex_unlock(pthread_mutex_t * mutex );
intpthread_mutex_trylock(pthread_mutex_t * mutex );//非阻塞式
intpthread_mutex_timedlock(pthread_mutex_t mutex, const struct timespec *tsptr);
返回值: 成功則返回 0, 出錯則返回錯誤編號。
對共享資源的訪問, 要對互斥量進行加鎖, 如果互斥量已經上了鎖, 呼叫執行緒會阻塞, 直到互斥量被解鎖。 在完成了對共享資源的訪問後, 要對互斥量進行解鎖。
Demo5:互斥鎖的應用
//使用互斥量解決多執行緒搶佔資源的問題
#include
#include
#include
#include
#include
char* buf[5]; //字元指標陣列 全域性變數
int pos; //用於指定上面陣列的下標
//1。定義互斥量
pthread_mutex_t mutex;
void *task(void *p)
{
//3。使用互斥量進行加鎖
// pthread_mutex_lock(&mutex);
buf[pos] = (char *)p;
usleep(200); //耗時操作
pos++;
//4。使用互斥量進行解鎖
// pthread_mutex_unlock(&mutex);
}
intmain(void)
{
//2。初始化互斥量, 預設屬性
pthread_mutex_init(&mutex, NULL);
//1。啟動一個執行緒 向陣列中儲存內容
pthread_t tid, tid2;
pthread_create(&tid, NULL, task, (void *)“str1”);
pthread_create(&tid2, NULL, task, (void *)“str2”);
//2。主執行緒程序等待,並且列印最終的結果
pthread_join(tid, NULL);
pthread_join(tid2, NULL);
//5。銷燬互斥量
pthread_mutex_destroy(&mutex);
int i = 0;
printf(“字元指標陣列中的內容是:”);
for(i = 0; i < pos; ++i)
{
printf(“%s ”, buf[i]);
}
printf(“\n”);
return0;
}
Demo中註釋掉了互斥鎖,執行結果如下
$ 。/main
字元指標陣列中的內容是:str1 (null)
Demo中建立了兩個執行緒用來給buf賦值字串,期望的效果是第一個執行緒給buf[0]賦值‘str1‘,第二個執行緒給buf[0]賦值‘str2’,當出現耗時操作的時候同時給buf[0]賦值‘str1’和‘str2’,與期望不符
加上互斥鎖之後,執行結果如下
$ 。/main
字元指標陣列中的內容是:str2 str1
讀寫鎖
讀寫鎖與互斥量類似,不過讀寫鎖允許更改的並行性,也叫共享互斥鎖。
如果當前執行緒讀資料 則允許其他執行緒進行讀操作 但不允許寫操作
如果當前執行緒寫資料 則其他執行緒的讀寫都不允許操作
例如對資料庫資料的讀寫應用:為了滿足當前能夠允許多個讀出,但只允許一個寫入的需求,執行緒提供了讀寫鎖來實現。
與讀寫鎖相關的API函式如下所示
#include
intpthread_rwlock_init(pthread_rwlock_t *rwlock,constpthread_rwlockattr_t *attr);
intpthread_rwlock_rdlock(pthread_rwlock_t *rwlock ); //非阻塞式
intpthread_rwlock_wrlock(pthread_rwlock_t *rwlock ); //非阻塞式
intpthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); //阻塞式
intpthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); //阻塞式
intpthread_rwlock_unlock(pthread_rwlock_t *rwlock);
intpthread_rwlock_destroy(pthread_rwlock_t *rwlock);
讀寫鎖的使用和互斥鎖類似,接下來Demo簡單演示下
Demo建立了四個執行緒,兩個讀執行緒,兩個寫執行緒,當寫執行緒搶到鎖之後,讀取使用者輸入(有人在寫),這個時候其他讀寫鎖都不能鎖定,當用戶輸入完之後,其他執行緒搶鎖,讀執行緒搶到鎖之後,只有另一個讀執行緒才可以搶到鎖,寫執行緒不可以搶到鎖。
Demo6:讀寫鎖的應用
#include
#include
#include
#include
#include
#include
staticpthread_rwlock_t rwlock;
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int time_to_exit;
void *thread_function_read_o(void *arg);
void *thread_function_read_t(void *arg);
void *thread_function_write_o(void *arg);
void *thread_function_write_t(void *arg);
intmain(int argc,char *argv[])
{
int res;
pthread_t a_thread,b_thread,c_thread,d_thread;
void *thread_result;
res=pthread_rwlock_init(&rwlock,NULL);
res = pthread_create(&a_thread, NULL, thread_function_read_o, NULL);//create new thread
res = pthread_create(&b_thread, NULL, thread_function_read_t, NULL);//create new thread
res = pthread_create(&c_thread, NULL, thread_function_write_o, NULL);//create new thread
res = pthread_create(&d_thread, NULL, thread_function_write_t, NULL);//create new thread
res = pthread_join(a_thread, &thread_result);
res = pthread_join(b_thread, &thread_result);
res = pthread_join(c_thread, &thread_result);
res = pthread_join(d_thread, &thread_result);
pthread_rwlock_destroy(&rwlock);
exit(EXIT_SUCCESS);
}
void *thread_function_read_o(void *arg)
{
while(strncmp(“end”, work_area, 3) != 0)
{
pthread_rwlock_rdlock(&rwlock);
printf(“this is thread read one。”);
printf(“read characters is %s”,work_area);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
pthread_rwlock_unlock(&rwlock);
time_to_exit=1;
pthread_exit(0);
}
void *thread_function_read_t(void *arg)
{
while(strncmp(“end”, work_area, 3) != 0)
{
pthread_rwlock_rdlock(&rwlock);
printf(“this is thread read two。”);
printf(“read characters is %s”,work_area);
pthread_rwlock_unlock(&rwlock);
sleep(2);
}
time_to_exit=1;
pthread_exit(0);
}
void *thread_function_write_o(void *arg)
{
while(!time_to_exit)
{
pthread_rwlock_wrlock(&rwlock);
printf(“this is write thread one。\nInput some text。\n”);
fgets(work_area, WORK_SIZE, stdin);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
pthread_rwlock_unlock(&rwlock);
pthread_exit(0);
}
void *thread_function_write_t(void *arg)
{
while(!time_to_exit)
{
pthread_rwlock_wrlock(&rwlock);
printf(“this is write thread two。\nInput some text。\n”);
fgets(work_area, WORK_SIZE, stdin);
pthread_rwlock_unlock(&rwlock);
sleep(2);
}
pthread_rwlock_unlock(&rwlock);
pthread_exit(0);
}
可以自行執行試一下效果。
條件變數
條件變數(cond)使在多執行緒程式中用來實現“等待——->喚醒”邏輯常用的方法,是程序間同步的一種機制。條件變數用來阻塞一個執行緒,直到條件滿足被觸發為止,通常情況下條件變數和互斥量同時使用。
一般條件變數有兩個狀態:
一個/多個執行緒為等待“條件變數的條件成立“而掛起;
另一個執行緒在“條件變數條件成立時”通知其他執行緒。
條件變數的型別 pthread_cond_t
#include
intpthread_cond_init(pthread_cond_t *restrict cond, constpthread_condattr_t *restrict attr);
intpthread_cond_destroy(pthread_cond_t *cond);
intpthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);//阻塞等待條件變數
intpthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);//超時等待
intpthread_cond_signal(pthread_cond_t *cond); //喚醒一個或者多個等待的執行緒
intpthread_cond_broadcast(pthread_cond_t *cond);//喚醒所有的等待的執行緒
條件變數透過允許執行緒阻塞和等待另一個執行緒傳送訊號,可以解決消費者和生產者的關係
案例如下:
生產者消費者模型
Demo7:生產者消費者模型
#include
#include
#include
#include
#include“pthread。h”
#define BUFFER_SIZE 2
/*生產者*/
structproducons
{
int buffer[BUFFER_SIZE]; /*資料*/
pthread_mutex_t lock; //互斥鎖
int readpos,writepos; //讀寫位置
pthread_cond_t nottempty; //條件變數 非空
pthread_cond_t notfull; //條件變數 非滿
};
structproduconsbuffer;//生產者物件
/*生產者初始化函式*/
voidinit(struct producons *prod)
{
pthread_mutex_init(&prod->lock,NULL); //初始化互斥鎖
pthread_cond_init(&prod->nottempty,NULL); //初始化條件變數
pthread_cond_init(&prod->notfull,NULL); //初始化條件變數
prod->readpos = 0;
prod->writepos = 0;
}
//生產訊息
voidput(struct producons * prod,int data)
{
pthread_mutex_lock(&prod->lock); //加鎖
//write until buffer not full
while((prod->writepos + 1)%BUFFER_SIZE == prod->readpos)
{
printf(“生產者等待生產,直到buffer有空位置\n”);
pthread_cond_wait(&prod->notfull,&prod->lock);
}
//將資料寫入到buffer裡面去
prod->buffer[prod->writepos] = data;
prod->writepos++;
if(prod->writepos >= BUFFER_SIZE)
prod->writepos = 0;
//觸發非空條件變數 告訴消費者可以消費
pthread_cond_signal(&prod->nottempty);
pthread_mutex_unlock(&prod->lock); //解鎖
}
//生產者執行緒
void * producer(void * data)
{
int n;
for(n = 0;n<5;n++)
{
printf(“生產者睡眠 1s。。。\n”);
sleep(1);
printf(“生產資訊:%d\n”, n);
put(&buffer, n);
}
for(n=5; n<10; n++)
{
printf(“生產者睡眠 3s。。。\n”);
sleep(3);
printf(“生產資訊:%d\n”,n);
put(&buffer,n);
}
put(&buffer, -1);
printf(“結束生產者!\n”);
returnNULL;
}
//消費訊息
intget(struct producons *prod)
{
int data;
pthread_mutex_lock(&prod->lock); //加鎖
while(prod->writepos == prod->readpos)
{
printf(“消費者等待,直到buffer有訊息\n”);
pthread_cond_wait(&prod->nottempty,&prod->lock);
}
//讀取buffer裡面的訊息
data = prod->buffer[prod->readpos];
prod->readpos++;
if(prod->readpos >=BUFFER_SIZE)
prod->readpos = 0;
//觸發非滿條件變數 告訴生產者可以生產
pthread_cond_signal(&prod->notfull);
pthread_mutex_unlock(&prod->lock); //解鎖
return data;
}
//消費者執行緒
void * consumer(void * data)
{
int d = 0;
while(1)
{
printf(“消費者睡眠 2s。。。\n”);
sleep(2);
d = get(&buffer);
printf(“讀取資訊:%d\n”,d);
if(d == -1) break;
}
printf(“結束消費者!\n”);
returnNULL;
}
intmain(int argc ,char *argv[])
{
pthread_t th_a,th_b; //定義a,b兩個執行緒
void * retval; //執行緒引數
init(&buffer);
pthread_create(&th_a,NULL,producer,0); //建立生產者執行緒
pthread_create(&th_b,NULL,consumer,0); //建立消費者執行緒
pthread_join(th_a,&retval); //等待a執行緒返回
pthread_join(th_b,&retval); //等待b執行緒返回
return0;
}
執行效果如下(擷取):
。。。
消費者等待,直到buffer有訊息
生產資訊:8
生產者睡眠 3s。。。
讀取資訊:8
消費者睡眠 2s。。。
消費者等待,直到buffer有訊息
生產資訊:9
生產者等待生產,直到buffer有空位置
讀取資訊:9
消費者睡眠 2s。。。
結束生產者!
讀取資訊:-1
結束消費者!
在這個Demo中,生產者生產貨物(資料)到倉庫(緩衝區),消費者從倉庫消費貨物,當倉庫已滿時通知生產者,生產者呼叫pthread_cond_wait阻塞等待條件變數notfull,這個條件變數由消費者喚醒;當倉庫非空的時候通知消費者,消費者呼叫pthread_cond_wait阻塞等待條件變數nottempty,這個條件變數由生產者喚醒。
·················END·················