Step by Step:Linux C多線程編程入門(基本API及多線程的同步與互斥)

 

介紹:什麼是線程,線程的優勢是什麼

線程在Unix系統下,一般被稱爲輕量級的進程,線程雖然不是進程,但卻能夠看做是Unix進程的表親,同一進程中的多條線程將共享該進程中的所有系統資源,如虛擬地址空間,文件描述符和信號處理等等。但同一進程中的多個線程有各自的調用棧(call stack),本身的寄存器環境(register context),本身的線程本地存儲(thread-local storage)。 一個進程能夠有不少線程,每條線程並行執行不一樣的任務。html

線程能夠提升應用程序在多核環境下處理諸如文件I/O或者socket I/O等會產生堵塞的狀況的表現性能。在Unix系統中,一個進程包含不少東西,包括可執行程序以及一大堆的諸如文件描述符地址空間等資源。在不少狀況下,完成相關任務的不一樣代碼間須要交換數據。若是採用多進程的方式,那麼通訊就須要在用戶空間和內核空間進行頻繁的切換,開銷很大。可是若是使用多線程的方式,由於可使用共享的全局變量,因此線程間的通訊(數據交換)變得很是高效。編程

Hello World(線程建立、結束、等待)

建立線程 pthread_create

線程建立函數包含四個變量,分別爲: 1. 一個線程變量名,被建立線程的標識 2. 線程的屬性指針,缺省爲NULL便可 3. 被建立線程的程序代碼 4. 程序代碼的參數 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);數據結構

結束線程 pthread_exit

線程結束調用實例:pthread_exit(void *retval); //retval用於存放線程結束的退出狀態多線程

線程等待 pthread_join

pthread_create調用成功之後,新線程和老線程誰先執行,誰後執行用戶是不知道的,這一塊取決與操做系統對線程的調度,若是咱們須要等待指定線程結束,須要使用pthread_join函數,這個函數實際上相似與多進程編程中的waitpid。 舉個例子,如下假設 A 線程調用 pthread_join 試圖去操做B線程,該函數將A線程阻塞,直到B線程退出,當B線程退出之後,A線程會收集B線程的返回碼。 該函數包含兩個參數:socket

  • pthread_t th //th是要等待結束的線程的標識
  • void **thread_return //指針thread_return指向的位置存放的是終止線程的返回狀態。

調用實例:pthread_join(thrd1, NULL);ide

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時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     // 線程建立成功,返回0,失敗返回失敗號
28     if (ret_thrd1 != 0) {
29         printf("線程1建立失敗\n");
30     } else {
31         printf("線程1建立成功\n");
32     }
33 
34     if (ret_thrd2 != 0) {
35         printf("線程2建立失敗\n");
36     } else {
37         printf("線程2建立成功\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,要否則會報錯,由於源代碼裏引用了pthread.h裏的東西,因此在gcc進行連接的時候,必需要找到這些庫的二進制實現代碼。函數

運行結果

運行結果 結果分析: 1.這段程序我運行了兩次,能夠看到,兩次的運行結果是不同的,從而說明,新線程和老線程誰先執行,誰後執行用戶是不知道的,這一塊取決與操做系統對線程的調度。 2.另外,咱們看到,在thread2的join結果出現了錯誤,打印出cannot join with thread2其實這個是個小錯誤,由於,我pthread_join傳進去的th是thread1,在上面的結果中,thread1早已經結束了,因此咱們再次等待thread1結束確定會出現沒法取到狀態的錯誤的。 3.pthread_join(thread1, &retval)確實等待了thread1的結束,咱們看到,在print_message_function函數循環了5遍結束之後,纔打印出thread1 endpost

這是一個很是簡單的例子,hello world級別的,只是用來演示Linux下C多線程的使用,在實際應用中,因爲多個線程每每會訪問共享的資源(典型的是訪問同一個全局變量),所以多個縣城間存在着競爭的關係,這就須要對多個線程進行同步,對其訪問的數據予以保護。性能

多線程的同步與互斥

方式一:鎖

  • 在主線程中初始化鎖爲解鎖狀態
    • pthread_mutex_t mutex;
    • pthread_mutex_init(&mutex, NULL);
  • 在編譯時初始化鎖爲解鎖狀態
    • 鎖初始化 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 訪問對象時的加鎖操做與解鎖操做
    • 加鎖 pthread_mutex_lock(&mutex)
    • 釋放鎖 pthread_mutex_unlock(&mutex)
不加鎖,數據不一樣步

咱們先來看一個不加鎖,多個線程訪問同一段數據的程序。

 1 /*************************************************************************
 2     > File Name: no_mutex.c
 3     > Author: couldtt(fyby)
 4     > Mail: fuyunbiyi@gmail.com
 5     > Created Time: 2013年12月15日 星期日 17時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

運行分析

不加鎖

從上圖可知,咱們no_mutex每次的運行結果都不一致,並且,運行結果也不符合咱們的預期,出現了錯誤的結果。 緣由就是三個線程競爭訪問全局變量sharedi,而且都沒有進行相應的同步。

舉個例子,當線程thrd1訪問到sharedi的時候,sharedi的值是1000,而後線程thrd1將sharedi的值累加到了1001,但是線程thrd2取到sharedi的時候,sharedi的值是1000,這時候線程thrd2對sharedi的值進行加1操做,使其變成了1001,但是這個時候,sharedi的值已經被線程thrd1加到1001了,然而,thrd2並不知道,因此又將sharedi的值賦爲了1001,從而致使告終果的錯誤。

這樣,咱們就須要一個線程互斥的機制,來保護sharedi這個變量,讓同一時刻,只有一個線程可以訪問到這個變量,從而使它的值可以保證正確的變化。

加鎖,數據同步

經過加鎖,保證sharedi變量在進行變動的時候,只有一個線程可以取到,並在在該線程對其進行操做的時候,其它線程沒法對其進行訪問。

 1 /*************************************************************************
 2     > File Name: mutex.c 
 3     > Author: couldtt(fyby)
 4     > Mail: fuyunbiyi@gmail.com 
 5     > Created Time: 2013年12月15日 星期日 17時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 }

 

結果分析

加鎖

這一次,咱們的結果是正確的,鎖有效得保護了咱們的數據安全。然而:

  1. 鎖保護的並非咱們的共享變量(或者說是共享內存),對於共享的內存而言,用戶是沒法直接對其保護的,由於那是物理內存,沒法阻止其餘程序的代碼訪問。事實上,鎖之因此對關鍵區域進行了保護,在本例中,是由於全部線程都遵循了一個規則,那就是在進入關鍵區域錢加同一把鎖,在退出關鍵區域錢釋放同一把

  2. 咱們從上述運行結果中能夠看到,加鎖是會帶來額外的開銷的,加鎖的代碼其運行速度,明顯比不加鎖的要慢一些,因此,在使用鎖的時候,要合理,在不須要對關鍵區域進行保護的場景下,咱們便不要多此一舉,爲其加鎖了

方式二:信號量

鎖有一個很明顯的缺點,那就是它只有兩種狀態:鎖定與不鎖定。

信號量本質上是一個非負數的整數計數器,它也被用來控制對公共資源的訪問。當公共資源增長的時候,調用信號量增長函數sem_post()對其進行增長,當公共資源減小的時候,調用函數sem_wait()來減小信號量。其實,咱們是能夠把鎖看成一個0-1信號量的。

它們是在/usr/include/semaphore.h中進行定義的,信號量的數據結構爲sem_t, 本質上,它是一個long型整數

相關函數

在使用semaphore以前,咱們須要先引入頭文件#include <semaphore.h>

  • 初始化信號量: int sem_init(sem_t *sem, int pshared, unsigned int value);
    • 成功返回0,失敗返回-1
    • 參數
    • sem:指向信號量結構的一個指針
    • pshared: 不是0的時候,該信號量在進程間共享,不然只能爲當前進程的全部線程們共享
    • value:信號量的初始值
  • 信號量減1操做,當sem=0的時候該函數會堵塞 int sem_wait(sem_t *sem);
    • 成功返回0,失敗返回-1
    • 參數
    • sem:指向信號量的一個指針
  • 信號量加1操做 int sem_post(sem_t *sem);
    • 參數與返回同上
  • 銷燬信號量 int sem_destroy(sem_t *sem);
    • 參數與返回同上
代碼示例
 1 /*************************************************************************
 2     > File Name: sem.c
 3     > Author: couldtt(fyby)
 4     > Mail: fuyunbiyi@gmail.com 
 5     > Created Time: 2013年12月15日 星期日 19時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 // 生產者
20 void provide_data(void) {
21     int i;
22     for (i=0; i< MAXSIZE; i++) {
23         stack[i] = i;
24         sem_post(&sem); //爲信號量加1
25     }
26 }
27 
28 // 消費者
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); //信號量初始化
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); //銷燬信號量
48 
49     return 0;
50 }

 

運行結果:

信號量的使用

由於信號量機制的存在,因此代碼在handle_data的時候,若是sem_wait(&sem)時,sem爲0,那麼代碼會堵塞在sem_wait上面,從而避免了在stack中訪問錯誤的index而使整個程序崩潰。

參考資料

相關文章
相關標籤/搜索