1. 程式人生 > >linux 線程詳解

linux 線程詳解

大於 linux下 blog 根據 影響 stack 復制代碼 系統資源 代碼

線程

是計算機中獨立運行的最小單位,運行時占用很少的系統資源。可以把線程看成是操作系統分配CPU時間的基本單元。一個進程可以擁有一個至多個線程。它線程在進程內部共享地址空間、打開的文件描述符等資源。同時線程也有其私有的數據信息,包括:線程號、寄存器(程序計數器和堆棧指針)、堆棧、信號掩碼、優先級、線程私有存儲空間。

為什麽有了進程的概念後,還要再引入線程呢?使用多線程到底有哪些好處?什麽的系統應該選用多線程?我們首先必須回答這些問題。
  使用多線程的理由之一是和進程相比,它是一種非常"節儉"的多任務操作方式。我們知道,在Linux系統下,啟動一個新的進程必須分配給它獨立的地址空間,建立眾多的數據表來維護它的代碼段、堆棧段和數據段,這是一種"昂貴"的多任務工作方式。而運行於一個進程中的多個線程,它們彼此之間使用相同的地址空間,共享大部分數據,啟動一個線程所花費的空間遠遠小於啟動一個進程所花費的空間,而且,線程間彼此切換所需的時間也遠遠小於進程間切換所需要的時間。據統計,總的說來,一個進程的開銷大約是一個線程開銷的30倍左右,當然,在具體的系統上,這個數據可能會有較大的區別。


  使用多線程的理由之二是線程間方便的通信機制。對不同進程來說,它們具有獨立的數據空間,要進行數據的傳遞只能通過通信的方式進行,這種方式不僅費時,而且很不方便。線程則不然,由於同一進程下的線程之間共享數據空間,所以一個線程的數據可以直接為其它線程所用,這不僅快捷,而且方便。當然,數據的共享也帶來其他一些問題,有的變量不能同時被兩個線程所修改,有的子程序中聲明為static的數據更有可能給多線程程序帶來災難性的打擊,這些正是編寫多線程程序時最需要註意的地方。


  除了以上所說的優點外,不和進程比較,多線程程序作為一種多任務、並發的工作方式,當然有以下的優點:
  1) 提高應用程序響應。這對圖形界面的程序尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程序不會響應鍵盤、鼠標、菜單的操作,而使用多線程技術,將耗時長的操作(time consuming)置於一個新的線程,可以避免這種尷尬的情況。
  2) 使多CPU系統更加有效。操作系統會保證當線程數不大於CPU數目時,不同的線程運行於不同的CPU上。
  3) 改善程序結構。一個既長又復雜的進程可以考慮分為多個線程,成為幾個獨立或半獨立的運行部分,這樣的程序會利於理解和修改。

創建線程

1 #include <pthread.h>
2 int pthread_create(pthread_t *thread,pthread_attr_t *attr,void *(*start_routine)(void *),void *arg);
3 pthread_t pthread_self(void);
4 int pthread_equal(pthread_t thread1,pthread_t thread2);
5 int pthread_once(pthread_once_t *once_control,void(*init_routine)(void));

linux系統支持POSIX多線程接口,稱為pthread。編寫linux下的多線程程序,需要包含頭文件pthread.h,鏈接時需要使用庫libpthread.a。

如果在主線程裏面創建線程,程序就會在創建線程的地方產生分支,變成兩個部分執行。線程的創建通過函數pthread_create來完成。成功返回0

參數:

thread: 參數是一個指針,當線程成功創建時,返回創建線程ID。

attr: 用於指定線程的屬性

start_routine: 該參數是一個函數指針,指向線程創建後要調用的函數。

arg: 傳遞給線程函數的參數。

一個簡單的創建線程程序:

$ cat main.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

int * thread(void *arg)
{
        printf("thread id is %d.\n",pthread_self());
        return NULL;
}

int main()
{
pthread_t id;
printf("Main thread id is %d \n",pthread_self());
if(!pthread_create(&id,NULL,(void *)thread,NULL))
{
printf("succeed!\n");
return 0;
}
else
{printf("Fail to Create Thread");
return -1;
}
}
$ ./main
Main thread id is 1
succeed!
thread id is 2.
 

線程終止

兩種方式終止線程。

第一通過return從線程函數返回,

第二種通過調用pthread_exit()函數使線程退出。

需要註意的地方:一是,主線程中如果從main函數返回或是調用了exit函數退出主線程,則整個進程終止,此時所有的其他線程也將終止。另一種是,如果主線程調用pthread_exit函數,則僅僅是主線程消亡,進程不會結束,其他線程也不會結束,知道所有的線程都結束時,進程才結束。

線程屬性

1 /* man pthread_attr_init */
2 typedef struct
3 {
4 int detachstate; //是否與其他線程脫離同步
5 int schedpolicy; //新線程的調度策略
6 struct sched_param schedparam; //運行優先級等
7 int inheritsched; //是否繼承調用者線程的值
8 int scope; //線程競爭CPU的範圍(優先級的範圍)
9 size_t guardsize; //警戒堆棧的大小
10 int stackaddr_set; //堆棧地址集
11 void * stackaddr; //堆棧地址
12 size_t stacksize; //堆棧大小
13 } pthread_attr_t;
屬性值不能直接設置,須使用相關函數進行操作,初始化的函數為pthread_attr_init,這個函數必須在pthread_create函數之前調用。

關於線程的綁定,牽涉到另外一個概念:輕進程(LWP:Light Weight Process)。輕進程可以理解為內核線程,它位於用戶層和系統層之間。系統對線程資源的分配、對線程的控制是通過輕進程來實現的,一個輕進程可以控制一個或多個線程。默認狀況下,啟動多少輕進程、哪些輕進程來控制哪些線程是由系統來控制的,這種狀況即稱為非綁定的。綁定狀況下,則顧名思義,即某個線程固定的"綁"在一個輕進程之上。被綁定的線程具有較高的響應速度,這是因為CPU時間片的調度是面向輕進程的,綁定的線程可以保證在需要的時候它總有一個輕進程可用。通過設置被綁定的輕進程的優先級和調度級可以使得綁定的線程滿足諸如實時反應之類的要求。
  設置線程綁定狀態的函數為pthread_attr_setscope,它有兩個參數,第一個是指向屬性結構的指針,第二個是綁定類型,它有兩個取值:PTHREAD_SCOPE_SYSTEM(綁定的)和PTHREAD_SCOPE_PROCESS(非綁定的)。下面的代碼即創建了一個綁定的線程。
#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;

/*初始化屬性值,均設為默認值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

pthread_create(&tid, &attr, (void *) my_function, NULL);

  線程的分離狀態決定一個線程以什麽樣的方式來終止自己。在上面的例子中,我們采用了線程的默認屬性,即為非分離狀態,這種情況下,原有的線程等待創建的線程結束。只有當pthread_join()函數返回時,創建的線程才算終止,才能釋放自己占用的系統資源。而分離線程不是這樣子的,它沒有被其他的線程所等待,自己運行結束了,線程也就終止了,馬上釋放系統資源。程序員應該根據自己的需要,選擇適當的分離狀態。設置線程分離狀態的函數為pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第二個參數可選為PTHREAD_CREATE_DETACHED(分離線程)和 PTHREAD _CREATE_JOINABLE(非分離線程)。這裏要註意的一點是,如果設置一個線程為分離線程,而這個線程運行又非常快,它很可能在pthread_create函數返回之前就終止了,它終止以後就可能將線程號和系統資源移交給其他的線程使用,這樣調用pthread_create的線程就得到了錯誤的線程號。要避免這種情況可以采取一定的同步措施,最簡單的方法之一是可以在被創建的線程裏調用pthread_cond_timewait函數,讓這個線程等待一會兒,留出足夠的時間讓函數pthread_create返回。設置一段等待時間,是在多線程編程裏常用的方法。但是註意不要使用諸如wait()之類的函數,它們是使整個進程睡眠,並不能解決線程同步的問題。
  另外一個可能常用的屬性是線程的優先級,它存放在結構sched_param中。用函數pthread_attr_getschedparam和函數pthread_attr_setschedparam進行存放,一般說來,我們總是先取優先級,對取得的值修改後再存放回去。

線程等待——正確處理線程終止

1 #include <pthread.h>
2 void pthread_exit(void *retval);
3 void pthread_join(pthread_t th,void *thread_return); //掛起等待th結束,*thread_return=retval;
4 int pthread_detach(pthread_t th);

線程只能被一個線程等待終止(第一個能正常返回),並且應處於join狀態(非DETACHED)。

在 Linux 平臺下,當處理線程結束時需要註意的一個問題就是如何讓一個線程善始善終,讓其所占資源得到正確釋放。在 Linux 平臺默認情況下,雖然各個線程之間是相互獨立的,一個線程的終止不會去通知或影響其他的線程。但是已經終止的線程的資源並不會隨著線程的終止而得到釋放,我們需要調用 pthread_join() 來獲得另一個線程的終止狀態並且釋放該線程所占的資源。

調用該函數的線程將掛起,等待 th 所表示的線程的結束。 thread_return 是指向線程 th 返回值的指針。需要註意的是 th 所表示的線程必須是 joinable 的,即處於非 detached(遊離)狀態;並且只可以有唯一的一個線程對 th 調用 pthread_join() 。如果 th 處於 detached 狀態,那麽對 th 的 pthread_join() 調用將返回錯誤。

如果你壓根兒不關心一個線程的結束狀態,那麽也可以將一個線程設置為 detached 狀態,從而來讓操作系統在該線程結束時來回收它所占的資源。將一個線程設置為 detached 狀態可以通過兩種方式來實現。一種是調用 pthread_detach() 函數,可以將線程 th 設置為 detached 狀態。其申明如清單 10 。

另一種方法是在創建線程時就將它設置為 detached 狀態,首先初始化一個線程屬性變量,然後將其設置為 detached 狀態,最後將它作為參數傳入線程創建函數 pthread_create(),這樣所創建出來的線程就直接處於 detached 狀態。方法如清單 11 。

創建 detach 線程:

pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, THREAD_FUNCTION, arg);

總之為了在使用 Pthread 時避免線程的資源在線程結束時不能得到正確釋放,從而避免產生潛在的內存泄漏問題,在對待線程結束時,要確保該線程處於 detached 狀態,否著就需要調用 pthread_join() 函數來對其進行資源回收。

線程私有數據

進程內的所有線程共享進程的數據空間,因此全局變量為所有線程所共有。但有時線程也需要保存自己的私有數據,這時可以創建線程私有數據(Thread-specific Date)TSD來解決。在線程內部,私有數據可以被各個函數訪問,但對其他線程是屏蔽的。例如我們常見的變量errno,它返回標準的出錯信息。它顯然不能是一個局部變量,幾乎每個函數都應該可以調用它;但它又不能是一個全局變量,否則在A線程裏輸出的很可能是B線程的出錯信息。要實現諸如此類的變量,我們就必須使用線程數據。我們為每個線程數據創建一個鍵,它和這個鍵相關聯,在各個線程裏,都使用這個鍵來指代線程數據,但在不同的線程裏,這個鍵代表的數據是不同的,在同一個線程裏,它代表同樣的數據內容。

線程私有數據采用了一鍵多值的技術,即一個鍵對應多個數值,訪問數據時好像是對同一個變量進行訪問,但其實是在訪問不同的數據。

創建私有數據的函數有4個:pthread_key_create(創建), pthread_setspecific(設置), pthread_getspecific(獲取), pthread_key_delete(刪除)。

1 #include <pthread.h>
2 int pthread_key_creadte(pthread_key_t *key,void (*destr_fuction) (void *));
3 int pthread_setspecific(pthread_key_t key,const void * pointer));
4 void * pthread_getspecific(pthread_key_t key);
5 int pthread_key_delete(ptherad_key_t key);

線程同步

線程的最大特點是資源的共享性,但資源共享中的同步問題是多線程編程的難點。linux下提供了多種方式來處理線程同步,最常用的是互斥鎖、條件變量和異步信號。

1)互斥鎖(mutex)

通過鎖機制實現線程間的同步。同一時刻只允許一個線程執行一個關鍵部分的代碼。

1 int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
2 int pthread_mutex_lock(pthread_mutex *mutex);
3 int pthread_mutex_destroy(pthread_mutex *mutex);
4 int pthread_mutex_unlock(pthread_mutex *
(1)先初始化鎖init()或靜態賦值pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIER attr_t有: PTHREAD_MUTEX_TIMED_NP:其余線程等待隊列 PTHREAD_MUTEX_RECURSIVE_NP:嵌套鎖,允許線程多次加鎖,不同線程,解鎖後重新競爭 PTHREAD_MUTEX_ERRORCHECK_NP:檢錯,與一同,線程請求已用鎖,返回EDEADLK; PTHREAD_MUTEX_ADAPTIVE_NP:適應鎖,解鎖後重新競爭 (2)加鎖,lock,trylock,lock阻塞等待鎖,trylock立即返回EBUSY (3)解鎖,unlock需滿足是加鎖狀態,且由加鎖線程解鎖 (4)清除鎖,destroy(此時鎖必需unlock,否則返回EBUSY,//Linux下互斥鎖不占用資源內存
$ cat main.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
int a;

int * thread(void *arg)
{
        printf("thread id is %d.\n",pthread_self());
        pthread_mutex_lock(&mutex);
        a=10;
        printf("a changed to %d.\n",a);
        pthread_mutex_unlock(&mutex);
        return NULL;
}

int main()
{
pthread_t id;
printf("Main thread id is %d \n",pthread_self());
a=3;
printf("In main func a=%d\n",a);

if(!pthread_create(&id,NULL,(void *)thread,NULL))
{
{
printf("Create thread succeed!\n");
}
else
{printf("Fail to Create Thread");
return -1;
}
pthread_join(&id,NULL);
pthread_mutex_destroy(&mutex);
return 0;
}

-bash-3.00$ ./main
Main thread id is 1
In main func a=3
Create thread succeed!
-bash-3.00$

2)條件變量(cond)

利用線程間共享的全局變量進行同步的一種機制。

技術分享 1 int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
2 int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
3 int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
4 int pthread_cond_destroy(pthread_cond_t *cond);
5 int pthread_cond_signal(pthread_cond_t *cond);
6 int pthread_cond_broadcast(pthread_cond_t *cond); //解除所有線程的阻塞
技術分享 (1)初始化.init()或者pthread_cond_t cond=PTHREAD_COND_INITIALIER;屬性置為NULL (2)等待條件成立.pthread_wait,pthread_timewait.wait()釋放鎖,並阻塞等待條件變量為真 timewait()設置等待時間,仍未signal,返回ETIMEOUT(加鎖保證只有一個線程wait) (3)激活條件變量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待線程) (4)清除條件變量:destroy;無線程等待,否則返回EBUSY

對於int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond,
pthread_mutex_t *mutex, const struct timespec *abstime);
一定要在mutex的鎖定區域內使用。

如果要正確的使用pthread_mutex_lock與pthread_mutex_unlock,請參考
pthread_cleanup_push和pthread_cleanup_pop宏,它能夠在線程被cancel的時候正確的釋放mutex!

另外,posix1標準說,pthread_cond_signal與pthread_cond_broadcast無需考慮調用線程是否是mutex的擁有者,也就是所,可以在lock與unlock以外的區域調用。如果我們對調用行為不關心,那麽請在lock區域之外調用吧。

1 #include <stdio.h>
2 #include <pthread.h>
3
4 pthread_mutex_t mutex;
5 pthread_cond_t cond;
6 void *thread1(void *arg)
7 {
8 pthread_cleanup_push(pthread_mutex_unlock,&mutex);
9 //提供函數回調保護
10 while(1){
11 printf("thread1 is running\n");
12 pthread_mutex_lock(&mutex);
13 pthread_cond_wait(&cond,&mutex);
14 printf("thread1 applied the condition\n");
15 pthread_mutex_unlock(&mutex);
16 sleep(4);
17 }
18 pthread_cleanup_pop(0);
19 }
20
21 void *thread2(void *arg)
22 {
23 while(1){
24 printf("thread2 is running\n");
25 pthread_mutex_lock(&mutex);
26 pthread_cond_wait(&cond,&mutex);
27 printf("thread2 applied the condition\n");
28 pthread_mutex_unlock(&mutex);
29 sleep(1);
30 }
31 }
32 int main()
33 {
34 pthread_t thid1,thid2;
35 printf("condition variable study!\n");
36 pthread_mutex_init(&mutex,NULL);
37 pthread_cond_init(&cond,NULL);
38 pthread_create(&thid1,NULL,(void*)thread1,NULL);
39 pthread_create(&thid2,NULL,(void*)thread2,NULL);
40 do{
41 pthread_cond_signal(&cond);
42 }while(1);
43 sleep(20);
44 pthread_exit(0);
45 return 0;

3)信號量

如同進程一樣,線程也可以通過信號量來實現通信,雖然是輕量級的。

信號量函數的名字都以"sem_"打頭。線程使用的基本信號量函數有四個。
#include <semaphore.h>
int sem_init (sem_t *sem , int pshared, unsigned int value);
這是對由sem指定的信號量進行初始化,設置好它的共享選項(linux 只支持為0,即表示它是當前進程的局部信號量),然後給它一個初始值VALUE。

兩個原子操作函數:
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
這兩個函數都要用一個由sem_init調用初始化的信號量對象的指針做參數。
sem_post:給信號量的值加1;
sem_wait:給信號量減1;對一個值為0的信號量調用sem_wait,這個函數將會等待直到有其它線程使它不再是0為止。

int sem_destroy(sem_t *sem);
這個函數的作用是再我們用完信號量後都它進行清理。歸還自己占有的一切資源。

linux 線程詳解