Step by Step:Linux C多線程編程入門(基本API及多線程的同步與互斥)
介紹:什么是線程,線程的優(yōu)點是什么
線程在Unix系統(tǒng)下,通常被稱為輕量級的進程,線程雖然不是進程,但卻可以看作是Unix進程的表親,同一進程中的多條線程將 共享該進程中的全部系統(tǒng)資源,如虛擬地址空間,文件描述符和信號處理等等 。但同一進程中的多個線程 有各自的調(diào)用棧(call stack),自己的寄存器環(huán)境(register context),自己的線程本地存儲(thread-local storage) 。 一個進程可以有很多線程,每條線程并行執(zhí)行不同的任務(wù)。
線程可以提高應(yīng)用程序在多核環(huán)境下處理諸如文件I/O或者socket I/O等會產(chǎn)生堵塞的情況的表現(xiàn)性能。在Unix系統(tǒng)中,一個進程包含很多東西,包括可執(zhí)行程序以及一大堆的諸如文件描述符地址空間等資源。在很多情況下,完成相關(guān)任務(wù)的不同代碼間需要交換數(shù)據(jù)。如果采用多進程的方式,那么通信就需要在用戶空間和內(nèi)核空間進行頻繁的切換,開銷很大。但是如果使用多線程的方式,因為可以使用共享的全局變量,所以線程間的通信(數(shù)據(jù)交換)變得非常高效。
Hello World(線程創(chuàng)建、結(jié)束、等待)
創(chuàng)建線程 pthread_create
線程創(chuàng)建函數(shù)包含四個變量,分別為: 1. 一個線程變量名,被創(chuàng)建線程的標識 2. 線程的屬性指針,缺省為NULL即可 3. 被創(chuàng)建線程的程序代碼 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é)束線程 pthread_exit
線程結(jié)束調(diào)用實例:
pthread_exit(void *retval);
?//retval用于存放線程結(jié)束的退出狀態(tài)
線程等待 pthread_join
pthread_create調(diào)用成功以后,新線程和老線程誰先執(zhí)行,誰后執(zhí)行用戶是不知道的,這一塊取決與操作系統(tǒng)對線程的調(diào)度,如果我們需要等待指定線程結(jié)束,需要使用pthread_join函數(shù),這個函數(shù)實際上類似與多進程編程中的waitpid。 舉個例子,以下假設(shè) A 線程調(diào)用 pthread_join 試圖去操作B線程,該函數(shù)將A線程阻塞,直到B線程退出,當B線程退出以后,A線程會收集B線程的返回碼。 該函數(shù)包含兩個參數(shù):
- pthread_t th //th是要等待結(jié)束的線程的標識
- void **thread_return //指針thread_return指向的位置存放的是終止線程的返回狀態(tài)。
調(diào)用實例:
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
//
線程創(chuàng)建成功,返回0,失敗返回失敗號
28
if
(ret_thrd1 !=
0
) {
29
printf(
"
線程1創(chuàng)建失敗\n
"
);
30
}
else
{
31
printf(
"
線程1創(chuàng)建成功\n
"
);
32
}
33
34
if
(ret_thrd2 !=
0
) {
35
printf(
"
線程2創(chuàng)建失敗\n
"
);
36
}
else
{
37
printf(
"
線程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
,要不然會報錯,因為源代碼里引用了pthread.h里的東西,所以在gcc進行鏈接的時候,必須要找到這些庫的二進制實現(xiàn)代碼。
運行結(jié)果
?結(jié)果分析: 1.這段程序我運行了兩次,可以看到,兩次的運行結(jié)果是不一樣的,從而說明,
新線程和老線程誰先執(zhí)行,誰后執(zhí)行用戶是不知道的,這一塊取決與操作系統(tǒng)對線程的調(diào)度
。 2.另外,我們看到,在thread2的join結(jié)果出現(xiàn)了錯誤,打印出
cannot join with thread2
其實這個是個小錯誤,因為,我pthread_join傳進去的th是thread1,在上面的結(jié)果中,thread1早已經(jīng)結(jié)束了,所以我們再次等待thread1結(jié)束肯定會出現(xiàn)無法取到狀態(tài)的錯誤的。 3.pthread_join(thread1, &retval)確實等待了thread1的結(jié)束,我們看到,在
print_message_function
函數(shù)循環(huán)了5遍結(jié)束以后,才打印出thread1 end
這是一個非常簡單的例子,hello world級別的,只是用來演示Linux下C多線程的使用,在實際應(yīng)用中,由于多個線程往往會訪問共享的資源(典型的是訪問同一個全局變量),因此多個縣城間存在著競爭的關(guān)系,這就需要對多個線程進行同步,對其訪問的數(shù)據(jù)予以保護。
多線程的同步與互斥
方式一:鎖
-
在主線程中初始化鎖為解鎖狀態(tài)
- pthread_mutex_t mutex;
- pthread_mutex_init(&mutex, NULL);
-
在編譯時初始化鎖為解鎖狀態(tài)
- 鎖初始化 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
-
訪問對象時的加鎖操作與解鎖操作
- 加鎖 pthread_mutex_lock(&mutex)
- 釋放鎖 pthread_mutex_unlock(&mutex)
不加鎖,數(shù)據(jù)不同步
我們先來看一個不加鎖,多個線程訪問同一段數(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時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每次的運行結(jié)果都不一致,而且,運行結(jié)果也不符合我們的預(yù)期,出現(xiàn)了錯誤的結(jié)果。 原因就是三個線程競爭訪問全局變量sharedi,并且都沒有進行相應(yīng)的同步。
舉個例子,當線程thrd1訪問到sharedi的時候,sharedi的值是1000,然后線程thrd1將sharedi的值累加到了1001,可是線程thrd2取到sharedi的時候,sharedi的值是1000,這時候線程thrd2對sharedi的值進行加1操作,使其變成了1001,可是這個時候,sharedi的值已經(jīng)被線程thrd1加到1001了,然而,thrd2并不知道,所以又將sharedi的值賦為了1001,從而導(dǎo)致了結(jié)果的錯誤。
這樣,我們就需要一個線程互斥的機制,來保護sharedi這個變量,讓同一時刻,只有一個線程能夠訪問到這個變量,從而使它的值能夠保證正確的變化。
加鎖,數(shù)據(jù)同步
通過加鎖,保證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
}
?
結(jié)果分析
這一次,我們的結(jié)果是正確的,鎖有效得保護了我們的數(shù)據(jù)安全。然而:
-
鎖保護的并不是我們的共享變量(或者說是共享內(nèi)存),對于共享的內(nèi)存而言,用戶是無法直接對其保護的,因為那是物理內(nèi)存,無法阻止其他程序的代碼訪問。事實上,鎖之所以對關(guān)鍵區(qū)域進行了保護,在本例中,是因為所有線程都遵循了一個規(guī)則,那就是在進入關(guān)鍵區(qū)域錢加
同一把鎖,在退出關(guān)鍵區(qū)域錢釋放同一把鎖 -
我們從上述運行結(jié)果中可以看到,加鎖是會帶來額外的開銷的,加鎖的代碼其運行速度,明顯比不加鎖的要慢一些,所以,在使用鎖的時候,要合理,在不需要對關(guān)鍵區(qū)域進行保護的場景下,我們便不要畫蛇添足,為其加鎖了
方式二:信號量
鎖有一個很明顯的缺點,那就是它
只有兩種狀態(tài)
:鎖定與不鎖定。
信號量本質(zhì)上是一個非負數(shù)的整數(shù)計數(shù)器,它也被用來控制對公共資源的訪問。當公共資源增加的時候,調(diào)用信號量增加函數(shù)sem_post()對其進行增加,當公共資源減少的時候,調(diào)用函數(shù)sem_wait()來減少信號量。其實,我們是可以把鎖當作一個0-1信號量的。
它們是在
/usr/include/semaphore.h
中進行定義的,信號量的數(shù)據(jù)結(jié)構(gòu)為sem_t, 本質(zhì)上,它是一個long型整數(shù)
相關(guān)函數(shù)
在使用semaphore之前,我們需要先引入頭文件
#include <semaphore.h>
-
初始化信號量:?
int sem_init(sem_t *sem, int pshared, unsigned int value);- 成功返回0,失敗返回-1
- 參數(shù)
- sem:指向信號量結(jié)構(gòu)的一個指針
- pshared: 不是0的時候,該信號量在進程間共享,否則只能為當前進程的所有線程們共享
- value:信號量的初始值
-
信號量減1操作,當sem=0的時候該函數(shù)會堵塞?
int sem_wait(sem_t *sem);- 成功返回0,失敗返回-1
- 參數(shù)
- sem:指向信號量的一個指針
-
信號量加1操作?
int sem_post(sem_t *sem);- 參數(shù)與返回同上
-
銷毀信號量?
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時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);
//
為信號量加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
}
?
運行結(jié)果:
因為信號量機制的存在,所以代碼在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語言應(yīng)用編程》(北京航空航天大學(xué)出版社)
- [3]? getting started with posix thread
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

