Step by Step:Linux C多線(xiàn)程編程入門(mén)(基本API及多線(xiàn)程的同步與互斥)
介紹:什么是線(xiàn)程,線(xiàn)程的優(yōu)點(diǎn)是什么
線(xiàn)程在Unix系統(tǒng)下,通常被稱(chēng)為輕量級(jí)的進(jìn)程,線(xiàn)程雖然不是進(jìn)程,但卻可以看作是Unix進(jìn)程的表親,同一進(jìn)程中的多條線(xiàn)程將 共享該進(jìn)程中的全部系統(tǒng)資源,如虛擬地址空間,文件描述符和信號(hào)處理等等 。但同一進(jìn)程中的多個(gè)線(xiàn)程 有各自的調(diào)用棧(call stack),自己的寄存器環(huán)境(register context),自己的線(xiàn)程本地存儲(chǔ)(thread-local storage) 。 一個(gè)進(jìn)程可以有很多線(xiàn)程,每條線(xiàn)程并行執(zhí)行不同的任務(wù)。
線(xiàn)程可以提高應(yīng)用程序在多核環(huán)境下處理諸如文件I/O或者socket I/O等會(huì)產(chǎn)生堵塞的情況的表現(xiàn)性能。在Unix系統(tǒng)中,一個(gè)進(jìn)程包含很多東西,包括可執(zhí)行程序以及一大堆的諸如文件描述符地址空間等資源。在很多情況下,完成相關(guān)任務(wù)的不同代碼間需要交換數(shù)據(jù)。如果采用多進(jìn)程的方式,那么通信就需要在用戶(hù)空間和內(nèi)核空間進(jìn)行頻繁的切換,開(kāi)銷(xiāo)很大。但是如果使用多線(xiàn)程的方式,因?yàn)榭梢允褂霉蚕淼娜肿兞浚跃€(xiàn)程間的通信(數(shù)據(jù)交換)變得非常高效。
Hello World(線(xiàn)程創(chuàng)建、結(jié)束、等待)
創(chuàng)建線(xiàn)程 pthread_create
線(xiàn)程創(chuàng)建函數(shù)包含四個(gè)變量,分別為: 1. 一個(gè)線(xiàn)程變量名,被創(chuàng)建線(xiàn)程的標(biāo)識(shí) 2. 線(xiàn)程的屬性指針,缺省為NULL即可 3. 被創(chuàng)建線(xiàn)程的程序代碼 4. 程序代碼的參數(shù) For example: - pthread_t thrd1; - pthread_attr_t? attr; - void thread_function(void? argument); - char *some_argument;
pthread_create(&thrd1, NULL, (void *)&thread_function, (void *) &some_argument);
結(jié)束線(xiàn)程 pthread_exit
線(xiàn)程結(jié)束調(diào)用實(shí)例:
pthread_exit(void *retval);
?//retval用于存放線(xiàn)程結(jié)束的退出狀態(tài)
線(xiàn)程等待 pthread_join
pthread_create調(diào)用成功以后,新線(xiàn)程和老線(xiàn)程誰(shuí)先執(zhí)行,誰(shuí)后執(zhí)行用戶(hù)是不知道的,這一塊取決與操作系統(tǒng)對(duì)線(xiàn)程的調(diào)度,如果我們需要等待指定線(xiàn)程結(jié)束,需要使用pthread_join函數(shù),這個(gè)函數(shù)實(shí)際上類(lèi)似與多進(jìn)程編程中的waitpid。 舉個(gè)例子,以下假設(shè) A 線(xiàn)程調(diào)用 pthread_join 試圖去操作B線(xiàn)程,該函數(shù)將A線(xiàn)程阻塞,直到B線(xiàn)程退出,當(dāng)B線(xiàn)程退出以后,A線(xiàn)程會(huì)收集B線(xiàn)程的返回碼。 該函數(shù)包含兩個(gè)參數(shù):
- pthread_t th //th是要等待結(jié)束的線(xiàn)程的標(biāo)識(shí)
- void **thread_return //指針thread_return指向的位置存放的是終止線(xiàn)程的返回狀態(tài)。
調(diào)用實(shí)例:
pthread_join(thrd1, NULL);
example1:
1 /* ************************************************************************ 2 > File Name: thread_hello_world.c 3 > Author: couldtt(fyby) 4 > Mail: fuyunbiyi@gmail.com 5 > Created Time: 2013年12月14日 星期六 11時(shí)48分50秒 6 *********************************************************************** */ 7 8 #include <stdio.h> 9 #include <stdlib.h> 10 #include <pthread.h> 11 12 void print_message_function ( void * ptr); 13 14 int main() 15 { 16 int tmp1, tmp2; 17 void * retval; 18 pthread_t thread1, thread2; 19 char *message1 = " thread1 " ; 20 char *message2 = " thread2 " ; 21 22 int ret_thrd1, ret_thrd2; 23 24 ret_thrd1 = pthread_create(&thread1, NULL, ( void *)&print_message_function, ( void * ) message1); 25 ret_thrd2 = pthread_create(&thread2, NULL, ( void *)&print_message_function, ( void * ) message2); 26 27 // 線(xiàn)程創(chuàng)建成功,返回0,失敗返回失敗號(hào) 28 if (ret_thrd1 != 0 ) { 29 printf( " 線(xiàn)程1創(chuàng)建失敗\n " ); 30 } else { 31 printf( " 線(xiàn)程1創(chuàng)建成功\n " ); 32 } 33 34 if (ret_thrd2 != 0 ) { 35 printf( " 線(xiàn)程2創(chuàng)建失敗\n " ); 36 } else { 37 printf( " 線(xiàn)程2創(chuàng)建成功\n " ); 38 } 39 40 // 同樣,pthread_join的返回值成功為0 41 tmp1 = pthread_join(thread1, & retval); 42 printf( " thread1 return value(retval) is %d\n " , ( int )retval); 43 printf( " thread1 return value(tmp) is %d\n " , tmp1); 44 if (tmp1 != 0 ) { 45 printf( " cannot join with thread1\n " ); 46 } 47 printf( " thread1 end\n " ); 48 49 tmp2 = pthread_join(thread1, & retval); 50 printf( " thread2 return value(retval) is %d\n " , ( int )retval); 51 printf( " thread2 return value(tmp) is %d\n " , tmp1); 52 if (tmp2 != 0 ) { 53 printf( " cannot join with thread2\n " ); 54 } 55 printf( " thread2 end\n " ); 56 57 } 58 59 void print_message_function( void * ptr ) { 60 int i = 0 ; 61 for (i; i< 5 ; i++ ) { 62 printf( " %s:%d\n " , ( char * )ptr, i); 63 } 64 }
?
編譯
gcc thread_hello_world.c -otest -lpthread
?一定要加上
-lpthread
,要不然會(huì)報(bào)錯(cuò),因?yàn)樵创a里引用了pthread.h里的東西,所以在gcc進(jìn)行鏈接的時(shí)候,必須要找到這些庫(kù)的二進(jìn)制實(shí)現(xiàn)代碼。
運(yùn)行結(jié)果
?結(jié)果分析: 1.這段程序我運(yùn)行了兩次,可以看到,兩次的運(yùn)行結(jié)果是不一樣的,從而說(shuō)明,
新線(xiàn)程和老線(xiàn)程誰(shuí)先執(zhí)行,誰(shuí)后執(zhí)行用戶(hù)是不知道的,這一塊取決與操作系統(tǒng)對(duì)線(xiàn)程的調(diào)度
。 2.另外,我們看到,在thread2的join結(jié)果出現(xiàn)了錯(cuò)誤,打印出
cannot join with thread2
其實(shí)這個(gè)是個(gè)小錯(cuò)誤,因?yàn)?我pthread_join傳進(jìn)去的th是thread1,在上面的結(jié)果中,thread1早已經(jīng)結(jié)束了,所以我們?cè)俅蔚却齮hread1結(jié)束肯定會(huì)出現(xiàn)無(wú)法取到狀態(tài)的錯(cuò)誤的。 3.pthread_join(thread1, &retval)確實(shí)等待了thread1的結(jié)束,我們看到,在
print_message_function
函數(shù)循環(huán)了5遍結(jié)束以后,才打印出thread1 end
這是一個(gè)非常簡(jiǎn)單的例子,hello world級(jí)別的,只是用來(lái)演示Linux下C多線(xiàn)程的使用,在實(shí)際應(yīng)用中,由于多個(gè)線(xiàn)程往往會(huì)訪問(wèn)共享的資源(典型的是訪問(wèn)同一個(gè)全局變量),因此多個(gè)縣城間存在著競(jìng)爭(zhēng)的關(guān)系,這就需要對(duì)多個(gè)線(xiàn)程進(jìn)行同步,對(duì)其訪問(wèn)的數(shù)據(jù)予以保護(hù)。
多線(xiàn)程的同步與互斥
方式一:鎖
-
在主線(xiàn)程中初始化鎖為解鎖狀態(tài)
- pthread_mutex_t mutex;
- pthread_mutex_init(&mutex, NULL);
-
在編譯時(shí)初始化鎖為解鎖狀態(tài)
- 鎖初始化 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
-
訪問(wèn)對(duì)象時(shí)的加鎖操作與解鎖操作
- 加鎖 pthread_mutex_lock(&mutex)
- 釋放鎖 pthread_mutex_unlock(&mutex)
不加鎖,數(shù)據(jù)不同步
我們先來(lái)看一個(gè)不加鎖,多個(gè)線(xiàn)程訪問(wèn)同一段數(shù)據(jù)的程序。
1 /* ************************************************************************ 2 > File Name: no_mutex.c 3 > Author: couldtt(fyby) 4 > Mail: fuyunbiyi@gmail.com 5 > Created Time: 2013年12月15日 星期日 17時(shí)52分24秒 6 *********************************************************************** */ 7 8 #include <stdio.h> 9 #include <stdlib.h> 10 #include <pthread.h> 11 12 int sharedi = 0 ; 13 void increse_num( void ); 14 15 int main(){ 16 int ret; 17 pthread_t thrd1, thrd2, thrd3; 18 19 ret = pthread_create(&thrd1, NULL, ( void * )increse_num, NULL); 20 ret = pthread_create(&thrd2, NULL, ( void * )increse_num, NULL); 21 ret = pthread_create(&thrd3, NULL, ( void * )increse_num, NULL); 22 23 pthread_join(thrd1, NULL); 24 pthread_join(thrd2, NULL); 25 pthread_join(thrd3, NULL); 26 27 printf( " sharedi = %d\n " , sharedi); 28 29 return 0 ; 30 31 } 32 33 void increse_num( void ) { 34 long i,tmp; 35 for (i= 0 ; i<= 100000 ; i++ ) { 36 tmp = sharedi; 37 tmp = tmp + 1 ; 38 sharedi = tmp; 39 } 40 }
?
編譯
gcc no_mutex.c -onomutex -lpthread
運(yùn)行分析
從上圖可知,我們no_mutex每次的運(yùn)行結(jié)果都不一致,而且,運(yùn)行結(jié)果也不符合我們的預(yù)期,出現(xiàn)了錯(cuò)誤的結(jié)果。 原因就是三個(gè)線(xiàn)程競(jìng)爭(zhēng)訪問(wèn)全局變量sharedi,并且都沒(méi)有進(jìn)行相應(yīng)的同步。
舉個(gè)例子,當(dāng)線(xiàn)程thrd1訪問(wèn)到sharedi的時(shí)候,sharedi的值是1000,然后線(xiàn)程thrd1將sharedi的值累加到了1001,可是線(xiàn)程thrd2取到sharedi的時(shí)候,sharedi的值是1000,這時(shí)候線(xiàn)程thrd2對(duì)sharedi的值進(jìn)行加1操作,使其變成了1001,可是這個(gè)時(shí)候,sharedi的值已經(jīng)被線(xiàn)程thrd1加到1001了,然而,thrd2并不知道,所以又將sharedi的值賦為了1001,從而導(dǎo)致了結(jié)果的錯(cuò)誤。
這樣,我們就需要一個(gè)線(xiàn)程互斥的機(jī)制,來(lái)保護(hù)sharedi這個(gè)變量,讓同一時(shí)刻,只有一個(gè)線(xiàn)程能夠訪問(wèn)到這個(gè)變量,從而使它的值能夠保證正確的變化。
加鎖,數(shù)據(jù)同步
通過(guò)加鎖,保證sharedi變量在進(jìn)行變更的時(shí)候,只有一個(gè)線(xiàn)程能夠取到,并在在該線(xiàn)程對(duì)其進(jìn)行操作的時(shí)候,其它線(xiàn)程無(wú)法對(duì)其進(jìn)行訪問(wèn)。
1 /* ************************************************************************ 2 > File Name: mutex.c 3 > Author: couldtt(fyby) 4 > Mail: fuyunbiyi@gmail.com 5 > Created Time: 2013年12月15日 星期日 17時(shí)52分24秒 6 *********************************************************************** */ 7 8 #include <stdio.h> 9 #include <stdlib.h> 10 #include <pthread.h> 11 12 int sharedi = 0 ; 13 void increse_num( void ); 14 15 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 16 17 int main(){ 18 int ret; 19 pthread_t thrd1, thrd2, thrd3; 20 21 ret = pthread_create(&thrd1, NULL, ( void * )increse_num, NULL); 22 ret = pthread_create(&thrd2, NULL, ( void * )increse_num, NULL); 23 ret = pthread_create(&thrd3, NULL, ( void * )increse_num, NULL); 24 25 pthread_join(thrd1, NULL); 26 pthread_join(thrd2, NULL); 27 pthread_join(thrd3, NULL); 28 29 printf( " sharedi = %d\n " , sharedi); 30 31 return 0 ; 32 33 } 34 35 void increse_num( void ) { 36 long i,tmp; 37 for (i= 0 ; i<= 100000 ; i++ ) { 38 /* 加鎖 */ 39 if (pthread_mutex_lock(&mutex) != 0 ) { 40 perror( " pthread_mutex_lock " ); 41 exit(EXIT_FAILURE); 42 } 43 tmp = sharedi; 44 tmp = tmp + 1 ; 45 sharedi = tmp; 46 /* 解鎖鎖 */ 47 if (pthread_mutex_unlock(&mutex) != 0 ) { 48 perror( " pthread_mutex_unlock " ); 49 exit(EXIT_FAILURE); 50 } 51 } 52 }
?
結(jié)果分析
這一次,我們的結(jié)果是正確的,鎖有效得保護(hù)了我們的數(shù)據(jù)安全。然而:
-
鎖保護(hù)的并不是我們的共享變量(或者說(shuō)是共享內(nèi)存),對(duì)于共享的內(nèi)存而言,用戶(hù)是無(wú)法直接對(duì)其保護(hù)的,因?yàn)槟鞘俏锢韮?nèi)存,無(wú)法阻止其他程序的代碼訪問(wèn)。事實(shí)上,鎖之所以對(duì)關(guān)鍵區(qū)域進(jìn)行了保護(hù),在本例中,是因?yàn)樗芯€(xiàn)程都遵循了一個(gè)規(guī)則,那就是在進(jìn)入關(guān)鍵區(qū)域錢(qián)加
同一把
鎖,在退出關(guān)鍵區(qū)域錢(qián)釋放同一把
鎖 -
我們從上述運(yùn)行結(jié)果中可以看到,加鎖是會(huì)帶來(lái)額外的開(kāi)銷(xiāo)的,加鎖的代碼其運(yùn)行速度,明顯比不加鎖的要慢一些,所以,在使用鎖的時(shí)候,要合理,在不需要對(duì)關(guān)鍵區(qū)域進(jìn)行保護(hù)的場(chǎng)景下,我們便不要畫(huà)蛇添足,為其加鎖了
方式二:信號(hào)量
鎖有一個(gè)很明顯的缺點(diǎn),那就是它
只有兩種狀態(tài)
:鎖定與不鎖定。
信號(hào)量本質(zhì)上是一個(gè)非負(fù)數(shù)的整數(shù)計(jì)數(shù)器,它也被用來(lái)控制對(duì)公共資源的訪問(wèn)。當(dāng)公共資源增加的時(shí)候,調(diào)用信號(hào)量增加函數(shù)sem_post()對(duì)其進(jìn)行增加,當(dāng)公共資源減少的時(shí)候,調(diào)用函數(shù)sem_wait()來(lái)減少信號(hào)量。其實(shí),我們是可以把鎖當(dāng)作一個(gè)0-1信號(hào)量的。
它們是在
/usr/include/semaphore.h
中進(jìn)行定義的,信號(hào)量的數(shù)據(jù)結(jié)構(gòu)為sem_t, 本質(zhì)上,它是一個(gè)long型整數(shù)
相關(guān)函數(shù)
在使用semaphore之前,我們需要先引入頭文件
#include <semaphore.h>
-
初始化信號(hào)量:?
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 成功返回0,失敗返回-1
- 參數(shù)
- sem:指向信號(hào)量結(jié)構(gòu)的一個(gè)指針
- pshared: 不是0的時(shí)候,該信號(hào)量在進(jìn)程間共享,否則只能為當(dāng)前進(jìn)程的所有線(xiàn)程們共享
- value:信號(hào)量的初始值
-
信號(hào)量減1操作,當(dāng)sem=0的時(shí)候該函數(shù)會(huì)堵塞?
int sem_wait(sem_t *sem);
- 成功返回0,失敗返回-1
- 參數(shù)
- sem:指向信號(hào)量的一個(gè)指針
-
信號(hào)量加1操作?
int sem_post(sem_t *sem);
- 參數(shù)與返回同上
-
銷(xiāo)毀信號(hào)量?
int sem_destroy(sem_t *sem);
- 參數(shù)與返回同上
代碼示例
1 /* ************************************************************************ 2 > File Name: sem.c 3 > Author: couldtt(fyby) 4 > Mail: fuyunbiyi@gmail.com 5 > Created Time: 2013年12月15日 星期日 19時(shí)25分08秒 6 *********************************************************************** */ 7 8 #include <stdio.h> 9 #include <unistd.h> 10 #include <pthread.h> 11 #include <semaphore.h> 12 13 #define MAXSIZE 10 14 15 int stack[MAXSIZE]; 16 int size = 0 ; 17 sem_t sem; 18 19 // 生產(chǎn)者 20 void provide_data( void ) { 21 int i; 22 for (i= 0 ; i< MAXSIZE; i++ ) { 23 stack[i] = i; 24 sem_post(&sem); // 為信號(hào)量加1 25 } 26 } 27 28 // 消費(fèi)者 29 void handle_data( void ) { 30 int i; 31 while ((i = size++) < MAXSIZE) { 32 sem_wait(& sem); 33 printf( " 乘法: %d X %d = %d\n " , stack[i], stack[i], stack[i]* stack[i]); 34 sleep( 1 ); 35 } 36 } 37 38 int main( void ) { 39 40 pthread_t provider, handler; 41 42 sem_init(&sem, 0 , 0 ); // 信號(hào)量初始化 43 pthread_create(&provider, NULL, ( void * )handle_data, NULL); 44 pthread_create(&handler, NULL, ( void * )provide_data, NULL); 45 pthread_join(provider, NULL); 46 pthread_join(handler, NULL); 47 sem_destroy(&sem); // 銷(xiāo)毀信號(hào)量 48 49 return 0 ; 50 }
?
運(yùn)行結(jié)果:
因?yàn)樾盘?hào)量機(jī)制的存在,所以代碼在handle_data的時(shí)候,如果sem_wait(&sem)時(shí),sem為0,那么代碼會(huì)堵塞在sem_wait上面,從而避免了在stack中訪問(wèn)錯(cuò)誤的index而使整個(gè)程序崩潰。
參考資料
- [1]? http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
- [2] 《Linux下C語(yǔ)言應(yīng)用編程》(北京航空航天大學(xué)出版社)
- [3]? getting started with posix thread
Step by Step:Linux C多線(xiàn)程編程入門(mén)(基本API及多線(xiàn)程的同步與互斥)
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
