多執行緒程式設計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);

多執行緒程式設計C語言版(附程式碼示例)

呼叫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);//喚醒所有的等待的執行緒

條件變數透過允許執行緒阻塞和等待另一個執行緒傳送訊號,可以解決消費者和生產者的關係

案例如下:

多執行緒程式設計C語言版(附程式碼示例)

生產者消費者模型

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,這個條件變數由生產者喚醒。

多執行緒程式設計C語言版(附程式碼示例)

·················END·················