——本文一個例子展開,介紹Linux下面線程的操做、多線程的同步和互斥。html
線程?爲何有了進程還須要線程呢,他們有什麼區別?使用線程有什麼優點呢?還有多線程編程的一些細節問題,如線程之間怎樣同步、互斥,這些東西將在本文中介紹。我在某QQ羣裏見到這樣一道面試題:面試
是否熟悉POSIX多線程編程技術?如熟悉,編寫程序完成以下功能:編程
1)有一int型全局變量g_Flag初始值爲0;ubuntu
2) 在主線稱中起動線程1,打印「this is thread1」,並將g_Flag設置爲1數據結構
3) 在主線稱中啓動線程2,打印「this is thread2」,並將g_Flag設置爲2多線程
4) 線程序1須要在線程2退出後才能退出併發
5) 主線程在檢測到g_Flag從1變爲2,或者從2變爲1的時候退出框架
咱們帶着這題開始這篇文章,結束以後,你們就都會作了。本文的框架以下:less
進程是程序執行時的一個實例,即它是程序已經執行到何種程度的數據結構的聚集。從內核的觀點看,進程的目的就是擔當分配系統資源(CPU時間、內存等)的基本單位。函數
線程是進程的一個執行流,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。一個進程由幾個線程組成(擁有不少相對獨立的執行流的用戶程序共享應用程序的大部分數據結構),線程與同屬一個進程的其餘的線程共享進程所擁有的所有資源。
"進程——資源分配的最小單位,線程——程序執行的最小單位"
進程有獨立的地址空間,一個進程崩潰後,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不一樣執行路徑。線程有本身的堆棧和局部變量,但線程沒有單獨的地址空間,一個線程死掉就等於整個進程死掉,因此多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行而且又要共享某些變量的併發操做,只能用線程,不能用進程。
從上面咱們知道了進程與線程的區別,其實這些區別也就是咱們使用線程的理由。總的來講就是:進程有獨立的地址空間,線程沒有單獨的地址空間(同一進程內的線程共享進程的地址空間)。(下面的內容摘自Linux下的多線程編程)
使用多線程的理由之一是和進程相比,它是一種很是"節儉"的多任務操做方式。咱們知道,在Linux系統下,啓動一個新的進程必須分配給它獨立的地址空間,創建衆多的數據表來維護它的代碼段、堆棧段和數據段,這是一種"昂貴"的多任務工做方式。而運行於一個進程中的多個線程,它們彼此之間使用相同的地址空間,共享大部分數據,啓動一個線程所花費的空間遠遠小於啓動一個進程所花費的空間,並且,線程間彼此切換所需的時間也遠遠小於進程間切換所須要的時間。據統計,總的說來,一個進程的開銷大約是一個線程開銷的30倍左右,固然,在具體的系統上,這個數據可能會有較大的區別。
使用多線程的理由之二是線程間方便的通訊機制。對不一樣進程來講,它們具備獨立的數據空間,要進行數據的傳遞只能經過通訊的方式進行,這種方式不只費時,並且很不方便。線程則否則,因爲同一進程下的線程之間共享數據空間,因此一個線程的數據能夠直接爲其它線程所用,這不只快捷,並且方便。固然,數據的共享也帶來其餘一些問題,有的變量不能同時被兩個線程所修改,有的子程序中聲明爲static的數據更有可能給多線程程序帶來災難性的打擊,這些正是編寫多線程程序時最須要注意的地方。
除了以上所說的優勢外,不和進程比較,多線程程序做爲一種多任務、併發的工做方式,固然有如下的優勢:
=============================
從函數調用上來講,進程建立使用fork()操做;線程建立使用clone()操做。Richard Stevens大師這樣說過:
fork is expensive. Memory is copied from the parent to the child, all descriptors are duplicated in the child, and so on. Current implementations use a technique called copy-on-write, which avoids a copy of the parent's data space to the child until the child needs its own copy. But, regardless of this optimization, fork is expensive.
IPC is required to pass information between the parent and child after the fork. Passing information from the parent to the child before the fork is easy, since the child starts with a copy of the parent's data space and with a copy of all the parent's descriptors. But, returning information from the child to the parent takes more work.
Threads help with both problems. Threads are sometimes called lightweight processes since a thread is "lighter weight" than a process. That is, thread creation can be 10–100 times faster than process creation.
All threads within a process share the same global memory. This makes the sharing of information easy between the threads, but along with this simplicity comes the problem of synchronization.
=============================
#include <pthread.h> int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func) (void *), void *arg); int pthread_join (pthread_t tid, void ** status); pthread_t pthread_self (void); int pthread_detach (pthread_t tid); void pthread_exit (void *status);
pthread_create用於建立一個線程,成功返回0,不然返回Exxx(爲正數)。
pthread_join用於等待某個線程退出,成功返回0,不然返回Exxx(爲正數)。
pthread_self用於返回當前線程的ID。
pthread_detach用因而指定線程變爲分離狀態,就像進程脫離終端而變爲後臺進程相似。成功返回0,不然返回Exxx(爲正數)。變爲分離狀態的線程,若是線程退出,它的全部資源將所有釋放。而若是不是分離狀態,線程必須保留它的線程ID,退出狀態直到其它線程對它調用了pthread_join。
進程也是相似,這也是當咱們打開進程管理器的時候,發現有不少僵死進程的緣由!也是爲何必定要有僵死這個進程狀態。
pthread_exit用於終止線程,能夠指定返回值,以便其餘線程經過pthread_join函數獲取該線程的返回值。
知道了這些函數以後,咱們試圖來完成本文一開始的問題:
1)有一int型全局變量g_Flag初始值爲0;
2)在主線稱中起動線程1,打印「this is thread1」,並將g_Flag設置爲1
3)在主線稱中啓動線程2,打印「this is thread2」,並將g_Flag設置爲2
這3點很簡單嘛!!!不就是調用pthread_create建立線程。代碼以下:
/* * 1)有一int型全局變量g_Flag初始值爲0; * * 2)在主線稱中起動線程1,打印「this is thread1」,並將g_Flag設置爲1 * * 3)在主線稱中啓動線程2,打印「this is thread2」,並將g_Flag設置爲2 * */ #include<stdio.h> #include<stdlib.h> #include<pthread.h> #include<errno.h> #include<unistd.h> int g_Flag=0; void* thread1(void*); void* thread2(void*); /* * when program is started, a single thread is created, called the initial thread or main thread. * Additional threads are created by pthread_create. * So we just need to create two thread in main(). */ int main(int argc, char** argv) { printf("enter main\n"); pthread_t tid1, tid2; int rc1=0, rc2=0; rc2 = pthread_create(&tid2, NULL, thread2, NULL); if(rc2 != 0) printf("%s: %d\n",__func__, strerror(rc2)); rc1 = pthread_create(&tid1, NULL, thread1, &tid2); if(rc1 != 0) printf("%s: %d\n",__func__, strerror(rc1)); printf("leave main\n"); exit(0); } /* * thread1() will be execute by thread1, after pthread_create() * it will set g_Flag = 1; */ void* thread1(void* arg) { printf("enter thread1\n"); printf("this is thread1, g_Flag: %d, thread id is %u\n",g_Flag, (unsigned int)pthread_self()); g_Flag = 1; printf("this is thread1, g_Flag: %d, thread id is %u\n",g_Flag, (unsigned int)pthread_self()); printf("leave thread1\n"); pthread_exit(0); } /* * thread2() will be execute by thread2, after pthread_create() * it will set g_Flag = 2; */ void* thread2(void* arg) { printf("enter thread2\n"); printf("this is thread2, g_Flag: %d, thread id is %u\n",g_Flag, (unsigned int)pthread_self()); g_Flag = 2; printf("this is thread1, g_Flag: %d, thread id is %u\n",g_Flag, (unsigned int)pthread_self()); printf("leave thread2\n"); pthread_exit(0); }
這樣就完成了1)、2)、3)這三點要求。編譯執行得以下結果:
netsky@ubuntu:~/workspace/pthead_test$ gcc -lpthread test.c
若是程序中使用到了pthread庫中的函數,除了要#include<pthread.h>,在編譯的時候還有加上-lpthread 選項。
netsky@ubuntu:~/workspace/pthead_test$ ./a.out
enter main
enter thread2
this is thread2, g_Flag: 0, thread id is 3079588720
this is thread1, g_Flag: 2, thread id is 3079588720
leave thread2
leave main
enter thread1
this is thread1, g_Flag: 2, thread id is 3071196016
this is thread1, g_Flag: 1, thread id is 3071196016
leave thread1
可是運行結果不必定是上面的,還有多是:
netsky@ubuntu:~/workspace/pthead_test$ ./a.out
enter main
leave main
enter thread1
this is thread1, g_Flag: 0, thread id is 3069176688
this is thread1, g_Flag: 1, thread id is 3069176688
leave thread1
或者是:
netsky@ubuntu:~/workspace/pthead_test$ ./a.out
enter main
leave main
等等。這也很好理解由於,這取決於主線程main函數什麼時候終止,線程thread一、thread2是否可以來得急執行它們的函數。這也是多線程編程時要注意的問題,由於有可能一個線程會影響到整個進程中的全部其它線程!若是咱們在main函數退出前,sleep()一段時間,就能夠保證thread一、thread2來得及執行。
Attention:你們確定已經注意到了,咱們在線程函數thread1()、thread2()執行完以前都調用了pthread_exit。若是我是調用exit()又或者是return會怎樣呢?本身動手試試吧!
pthread_exit()用於線程退出,能夠指定返回值,以便其餘線程經過pthread_join()函數獲取該線程的返回值。
return是函數返回,只有線程函數return,線程纔會退出。
exit是進程退出,若是在線程函數中調用exit,進程中的全部函數都會退出!
「4) 線程序1須要在線程2退出後才能退出」第4點也很容易解決,直接在thread1的函數退出以前調用pthread_join就OK了。
上面的代碼彷佛很好的解決了問題的前面4點要求,其實否則!!!由於g_Flag是一個全局變量,線程thread1和thread2能夠同時對它進行操做,須要對它進行加鎖保護,thread1和thread2要互斥訪問才行。下面咱們就介紹如何加鎖保護——互斥鎖。
互斥鎖:
使用互斥鎖(互斥)可使線程按順序執行。一般,互斥鎖經過確保一次只有一個線程執行代碼的臨界段來同步多個線程。互斥鎖還能夠保護單線程代碼。
互斥鎖的相關操做函數以下:
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t * mptr); int pthread_mutex_unlock(pthread_mutex_t * mptr); //Both return: 0 if OK, positive Exxx value on error
在對臨界資源進行操做以前須要pthread_mutex_lock先加鎖,操做完以後pthread_mutex_unlock再解鎖。並且在這以前須要聲明一個pthread_mutex_t類型的變量,用做前面兩個函數的參數。具體代碼見第5節。
第5點——主線程在檢測到g_Flag從1變爲2,或者從2變爲1的時候退出。就須要用到線程同步技術!線程同步須要條件變量。
條件變量:
使用條件變量能夠以原子方式阻塞線程,直到某個特定條件爲真爲止。條件變量始終與互斥鎖一塊兒使用。對條件的測試是在互斥鎖(互斥)的保護下進行的。
若是條件爲假,線程一般會基於條件變量阻塞,並以原子方式釋放等待條件變化的互斥鎖。若是另外一個線程更改了條件,該線程可能會向相關的條件變量發出信號,從而使一個或多個等待的線程執行如下操做:
- 喚醒
- 再次獲取互斥鎖
- 從新評估條件
在如下狀況下,條件變量可用於在進程之間同步線程:
- 線程是在能夠寫入的內存中分配的
- 內存由協做進程共享
「使用條件變量能夠以原子方式阻塞線程,直到某個特定條件爲真爲止。」便可用到第5點,主線程main函數阻塞於等待g_Flag從1變爲2,或者從2變爲1。條件變量的相關函數以下:
#include <pthread.h> int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr); int pthread_cond_signal(pthread_cond_t *cptr); //Both return: 0 if OK, positive Exxx value on error
pthread_cond_wait用於等待某個特定的條件爲真,pthread_cond_signal用於通知阻塞的線程某個特定的條件爲真了。在調用者兩個函數以前須要聲明一個pthread_cond_t類型的變量,用於這兩個函數的參數。
爲何條件變量始終與互斥鎖一塊兒使用,對條件的測試是在互斥鎖(互斥)的保護下進行的呢?由於「某個特性條件」一般是在多個線程之間共享的某個變量。互斥鎖容許這個變量能夠在不一樣的線程中設置和檢測。
一般,pthread_cond_wait只是喚醒等待某個條件變量的一個線程。若是須要喚醒全部等待某個條件變量的線程,須要調用:
int pthread_cond_broadcast (pthread_cond_t * cptr);
默認狀況下面,阻塞的線程會一直等待,知道某個條件變量爲真。若是想設置最大的阻塞時間能夠調用:
int pthread_cond_timedwait (pthread_cond_t * cptr, pthread_mutex_t *mptr, const struct timespec *abstime);
若是時間到了,條件變量尚未爲真,仍然返回,返回值爲ETIME。
經過前面的介紹,咱們能夠輕鬆的寫出代碼了,以下所示:
/* 是否熟悉POSIX多線程編程技術?如熟悉,編寫程序完成以下功能: 1)有一int型全局變量g_Flag初始值爲0; 2)在主線稱中起動線程1,打印「this is thread1」,並將g_Flag設置爲1 3)在主線稱中啓動線程2,打印「this is thread2」,並將g_Flag設置爲2 4)線程序1須要在線程2退出後才能退出 5)主線程在檢測到g_Flag從1變爲2,或者從2變爲1的時候退出 */ #include<stdio.h> #include<stdlib.h> #include<pthread.h> #include<errno.h> #include<unistd.h> typedef void* (*fun)(void*); int g_Flag=0; static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void* thread1(void*); void* thread2(void*); /* * when program is started, a single thread is created, called the initial thread or main thread. * Additional threads are created by pthread_create. * So we just need to create two thread in main(). */ int main(int argc, char** argv) { printf("enter main\n"); pthread_t tid1, tid2; int rc1=0, rc2=0; rc2 = pthread_create(&tid2, NULL, thread2, NULL); if(rc2 != 0) printf("%s: %d\n",__func__, strerror(rc2)); rc1 = pthread_create(&tid1, NULL, thread1, &tid2); if(rc1 != 0) printf("%s: %d\n",__func__, strerror(rc1)); pthread_cond_wait(&cond, &mutex); printf("leave main\n"); exit(0); } /* * thread1() will be execute by thread1, after pthread_create() * it will set g_Flag = 1; */ void* thread1(void* arg) { printf("enter thread1\n"); printf("this is thread1, g_Flag: %d, thread id is %u\n",g_Flag, (unsigned int)pthread_self()); pthread_mutex_lock(&mutex); if(g_Flag == 2) pthread_cond_signal(&cond); g_Flag = 1; printf("this is thread1, g_Flag: %d, thread id is %u\n",g_Flag, (unsigned int)pthread_self()); pthread_mutex_unlock(&mutex); pthread_join(*(pthread_t*)arg, NULL); printf("leave thread1\n"); pthread_exit(0); } /* * thread2() will be execute by thread2, after pthread_create() * it will set g_Flag = 2; */ void* thread2(void* arg) { printf("enter thread2\n"); printf("this is thread2, g_Flag: %d, thread id is %u\n",g_Flag, (unsigned int)pthread_self()); pthread_mutex_lock(&mutex); if(g_Flag == 1) pthread_cond_signal(&cond); g_Flag = 2; printf("this is thread2, g_Flag: %d, thread id is %u\n",g_Flag, (unsigned int)pthread_self()); pthread_mutex_unlock(&mutex); printf("leave thread2\n"); pthread_exit(0); }
編譯運行能夠獲得符合要求的結果!