介紹:什么是線程,線程的優點是什么
線程在Unix系統下,通常被稱為輕量級的進程,線程雖然不是進程,但卻可以看作是Unix進程的表親,同一進程中的多條線程將共享該進程中的全部系統資源,如虛擬地址空間,文件描述符和信號處理等等。但同一進程中的多個線程有各自的調用棧(call stack),自己的寄存器環境(register context),自己的線程本地存儲(thread-local storage)。 一個進程可以有很多線程,每條線程並行執行不同的任務。
線程可以提高應用程序在多核環境下處理諸如文件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線程的返回碼。 該函數包含兩個參數:
- pthread_t th //th是要等待結束的線程的標識
- void **thread_return //指針thread_return指向的位置存放的是終止線程的返回狀態。
調用實例: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時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 end
這是一個非常簡單的例子,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 }
結果分析

這一次,我們的結果是正確的,鎖有效得保護了我們的數據安全。然而:
-
鎖保護的並不是我們的共享變量(或者說是共享內存),對於共享的內存而言,用戶是無法直接對其保護的,因為那是物理內存,無法阻止其他程序的代碼訪問。事實上,鎖之所以對關鍵區域進行了保護,在本例中,是因為所有線程都遵循了一個規則,那就是在進入關鍵區域錢加
同一把鎖,在退出關鍵區域錢釋放同一把鎖 -
我們從上述運行結果中可以看到,加鎖是會帶來額外的開銷的,加鎖的代碼其運行速度,明顯比不加鎖的要慢一些,所以,在使用鎖的時候,要合理,在不需要對關鍵區域進行保護的場景下,我們便不要畫蛇添足,為其加鎖了
方式二:信號量
鎖有一個很明顯的缺點,那就是它只有兩種狀態:鎖定與不鎖定。
信號量本質上是一個非負數的整數計數器,它也被用來控制對公共資源的訪問。當公共資源增加的時候,調用信號量增加函數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而使整個程序崩潰。
參考資料
- [1] http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
- [2] 《Linux下C語言應用編程》(北京航空航天大學出版社)
- [3] getting started with posix thread
來源:https://www.cnblogs.com/fuyunbiyi/p/3475602.html

