C++多線程與多進程編程
-
Linux下的多線程庫:NGPT和NPTL,NPTL比linuxThread效率更高,且符合POSIX編程規範,因此通常都是用POSIX下的線程庫:pthread標準
NPTL的實現包括三個內容,創建線程和結束線程;讀取和設置線程屬性;POSIX線程同步的方式:POSIX 信號量,互斥條件和條件變量。
-
線程模型
線程是程序中完成一個獨立任務的完整執行序列,即一個可調度的實體。根據調度者的身份,線程可以分爲用戶線程和內核線程兩種
-
內核線程:運行在內核空間,由內核來調度
-
用戶線程:運行在用戶空間,由線程庫來調度。
當進程的一個內核線程獲得CPU的使用權時,他就加載並運行了一個用戶線程,因此,內核線程相當於用戶線程的運行“容器”,一個進程可以擁有M個內核線程和N個用戶線程,M<=N。
根據這個比值,有三種情況:完全在用戶空間實現多線程;完全在內核空間實現多線程;雙層調度。
完全在用戶空間實現的線程無需內核的支持,內核甚至不知道這些線程的存在。線程庫負責管理所有的線程,使用longjmp來切換線程的執行,使他們看起來像是“併發”執行的,但實際上內核仍然是把整個進程作爲最小單位來調度的,所以內核線程就是進程本身。
完全在內核空間實現的線程將創建,調度的任務都交給了內核,線程庫無需執行管理任務。
-
-
Linux下的線程庫最有名的就是LInuxThreads和NPTL,現在默認是NPTL。LinuxThreads線程庫的內核線程是使用clone系統調用創建進程模擬的,clone系統調用和fork系統調用過程類似,不過可以指定創建的子進程與調用進程共享相同的虛擬地址空間。用進程來模擬線程有許多缺點:
-
每個線程擁有不同的PID
-
LInux信號處理函數是基於進程的,現在一個進程內所有線程都能且必須處理信號。
-
系統的最大線程數就是最大進程數
等
LinuxThreads還提供管理線程,負責接收系統發送的信號,終止線程,阻塞線程和回收線程堆棧等工作,效率低下。
NPTL庫在linux內核的完善下應運而生,Linux內核提供了真正的內核線程,NPTL有如下優點:
-
內核線程不再是一個進程,進程的線程可以運行在不同的CPU上,由內核調度。
-
不存在管理線程,回收終止等工作都由內核完成
-
線程的同步由內核完成,隸屬於不同進程的線程之間也能共享互斥鎖,實現跨進程的同步。
-
-
線程創建
#include <pthread.h> int pthread_create(pthread_t *thread,const pthread_attr_t *attr,vid *(*start_routine)(void* ),void *arg);
pthread_t爲一個無符號長整型,爲線程的編號;arg參數用於設置線程的屬性,NULL爲默認。
該函數成功時返回0,失敗返回錯誤碼。
-
線程調度與停止
線程一旦創建好,內核就可以調度內核線程來執行start_routine函數指向的函數了。線程退出最好使用pthread_exit,確保安全,乾淨的退出。
#include <pthread.h> void pthread_exit(void *retval);//retval向回收者傳遞退出信息。
-
線程的回收
一個進程中的所有線程都可以通過調用pthread_join函數來回收其他線程,即等待其他線程結束,這類似於回收進程的wait和waitpid系統調用。
#include <pthread.h> int pthread_join(pthread_t thread,void *retval);
thread參數是目標的標識符,成功返回0,失敗返回錯誤碼:
EDEADLK:可能引起死鎖,比如兩個線程互相對對方調用pthread_join,或者線程對自身調用pthread_join EINVAL:目標線程是不可回收的,或者已經有其他線程在回收該線程 ESRCH:目標線程不存在
-
線程的終止與取消
一個線程可能出現異常,我們希望手動終止他,可以通過如下函數:
#include <pthread.h> int pthread_cancel(pthread_t thread);
成功返回0,失敗返回錯誤碼。
不過,接受到取消請求的目標線程可以決定是否允許取消以及如何取消,
#include <pthread.h> int pthread_setcancelstate(int state,int *oldstate); int pthread_setcanceltype(int type,int *oldtype);
這兩個函數的第一參數分別設置線程的取消狀態和取消類型;第二個參數則分別記錄線程原來的取消狀態和取消類型。更多細節請看API
-
線程的屬性
#include <pthread.h> #define __SIZEOF_PTHREAD_ATTR_T 36 typedef union{ char __size[__SIZEOF_PTHREAD_ATTR_T]; long int __align; }pthread_attr_t
各種線程屬性全部包含在這個字符數組中,可以通過一系列API來操作pthread_attr_t,以便獲取和設置線程的屬性:
1.pthread_attr_init 功能: 對線程屬性變量的初始化。
2.pthread_attr_setscope 功能: 設置線程 __scope 屬性...
3.pthread_attr_setdetachstate 功能: 設置線程detach...
4.pthread_attr_setschedparam 功能: 設置線程sched...
5.pthread_attr_getschedparam 功能: 得到線程優先級。
線程的屬性如下:
typedef struct { int detachstate; //線程的分離狀態 int schedpolicy; //線程的調度策略 struct sched schedparam;//線程的調度參數 int inheritsched; //線程的繼承性 int scope; //線程的作用域 size_t guardsize; //線程棧末尾的警戒緩衝區大小 int stackaddr_set; //線程棧的設置 void* stackaddr; //線程棧的啓始位置 size_t stacksize; //線程棧大小 }pthread_attr_t;
都有相應的函數進行設置。
-
POSIX信號量:處理多線程的同步問題
3種處理先吃之間同步問題的機制:POSIX信號量,互斥量和條件變量
LInux中,信號量有兩種,第一種是進程間通信使用的 System V IPC,另一種就是POSIX信號量:
#include <semaphore.h> //初始化信號量 int sem_init(sem_t *sem, int pshared,unsigned value); //銷燬信號量 int sem_destroy(sem_t *sem); //以原子操作的方式將信號量的值+1 int sem_post(sem_t *sem); //以原子操作的方式將信號量的值-1 int sem_wait(sem_t *sem); //與sem_wait相同,不過始終立即返回,如果未成功返回-1並將errno置爲EAGAIN int sem_trywait(sem_t *sem); //關閉命名信號量 int sem_close(sem_t *sem); //命名一個信號量 sem_t *sem_open(const char *name, int flag);
信號量的使用:(重點)
1.建立兩個線程,兩個線程分別將自己的整形數i從1遞增到100,並規定兩個線程的整形不能超過5
#include <bits/stdc++.h> #include <pthread.h> #include <semaphore.h> #include <unistd.h>//提供對POSIX操作系統API的訪問權限的頭文件 #include <windows.h> //using namespace std; const int INF=100; sem_t sem_1,sem_2;//兩個信號量 using std::cout; using std::endl; /* 思路:因爲最大差值不能超過5,先初始化兩個信號量爲5,如果一個數+1,在wait自己sem的 同時post另一個sem,表示你可以多加一點,因爲差值可以從[-5,5],所以必須post另一個sem才滿足條件 */ void* run1(void *arg){ int i;//變量 for(i=0;i<=INF;i++){ sem_wait(&sem_1);//原子操作-1 usleep(500*1000); cout<<"the thread 1 print "<<i<<endl; sem_post(&sem_2);//原子操作+1 } pthread_exit(0); } void* run2(void *arg){ int i; for(i=0;i<=INF;i++){ sem_wait(&sem_2); usleep(500*1000); cout<<"the thread 2 print "<<i<<endl; sem_post(&sem_1); } pthread_exit(0); } signed main(){ sem_init(&sem_1,0,5); sem_init(&sem_2,0,5); pthread_t tid1=5000,tid2=10000; pthread_create(&tid1,NULL,run1,NULL); pthread_create(&tid2,NULL,run2,NULL); pthread_join(tid1,NULL); pthread_join(tid2,NULL); sem_destroy(&sem_1); sem_destroy(&sem_2); return 0; }
2.經典線程問題:生產者消費者問題
#include <bits/stdc++.h> #include <unistd.h> #include <pthread.h> #include <windows.h> #include <semaphore.h> const int maxn=101; struct Stack{ int a[maxn]; int top; void init(){ memset(a,0,sizeof(a)); top=0; } void push(int num){ a[top++]=num; } int pop(){ return a[top--]; } }Q; sem_t Full,Empty; void* push(void *arg){ while(true){ sem_wait(&Full); int num=rand()%100000; printf("producter has produce an integer %d,now the top is %d\n",num,Q.top); Q.push(num); usleep(500*1000); sem_post(&Empty); } pthread_exit(0); } void *pop(void *arg){ while(true){ sem_wait(&Empty); printf("comsumer has comsume an integer %d,now the top is %d\n",Q.pop(),Q.top); usleep(500*1000); sem_post(&Full); } pthread_exit(0); } signed main(){ Q.init(); sem_init(&Full,0,100); sem_init(&Empty,0,0); pthread_t tid1,tid2; pthread_create(&tid1,NULL,push,NULL); pthread_create(&tid2,NULL,pop,NULL); pthread_join(tid1,NULL); pthread_join(tid2,NULL); sem_destroy(&Full); sem_destroy(&Empty); return 0; }
-
互斥鎖
互斥鎖可以用於保護關鍵代碼段,這有點向二進制的POSIX信號量
互斥鎖具有以下特點:
`
·
原子性:把一個互斥鎖定義爲一個原子操作,這意味着操作系統保證瞭如果一個線程鎖定了互斥鎖,則沒有其他線程可以在同一時間成功鎖定這個互斥量。``
·
唯一性:如果一個線程鎖定一個互斥量,在它接觸鎖定之前,沒有其他線程可以鎖定這個互斥量。``
·
非繁忙等待:如果一個線程已經鎖定了一個互斥鎖,第二個線程又試圖去鎖定這個互斥鎖,則第二個線程將被掛起(不佔用CPU
資源),直到第一個線程解鎖,第二個線程則被喚醒並繼續執行,同時鎖定這個互斥量`互斥鎖的使用:
-
在訪問共享資源後臨界區域前,對互斥鎖進行加鎖;
-
在訪問完成後釋放互斥鎖導上的鎖。在訪問完成後釋放互斥鎖導上的鎖;
-
對互斥鎖進行加鎖後,任何其他試圖再次對互斥鎖加鎖的線程將會被阻塞,直到鎖被釋放。對互斥鎖進行加鎖後,任何其他試圖再次對互斥鎖加鎖的線程將會被阻塞,直到鎖被釋放。
-
```c++ // 初始化一個互斥鎖。 int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); // 對互斥鎖上鎖,若互斥鎖已經上鎖,則調用者一直阻塞, // 直到互斥鎖解鎖後再上鎖。 int pthread_mutex_lock(pthread_mutex_t *mutex); // 調用該函數時,若互斥鎖未加鎖,則上鎖,返回 0; // 若互斥鎖已加鎖,則函數直接返回失敗,即 EBUSY。 int pthread_mutex_trylock(pthread_mutex_t *mutex); // 當線程試圖獲取一個已加鎖的互斥量時,pthread_mutex_timedlock 互斥量 // 原語允許綁定線程阻塞時間。即非阻塞加鎖互斥量。 int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout); // 對指定的互斥鎖解鎖。 int pthread_mutex_unlock(pthread_mutex_t *mutex); // 銷燬指定的一個互斥鎖。互斥鎖在使用完畢後, // 必須要對互斥鎖進行銷燬,以釋放資源。 int pthread_mutex_destroy(pthread_mutex_t *mutex); ``` 還有一個常用的方法就是:將互斥鎖封裝成對象,構成自動鎖,這樣生命週期結束的時候鎖自動銷燬 ```c++ class CAutoMutex { public: CAutoMutex() { mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&mutex); } ~CAutoMutex() { pthread_mutex_unlock(&mutex); } private: pthread_mutex_t mutex; }; ``` 死鎖產生:對已經加鎖的普通鎖再次加鎖,會構成死鎖;兩個對象按照不同的順序申請互斥鎖 ``` #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <string.h> #include <windows.h> #include <bits/stdc++.h> pthread_mutex_t mutex_a; pthread_mutex_t mutex_b; int a,b; const int INF=100; void* run(void *arg){ //鎖b int x=0; while(x<=INF){ pthread_mutex_lock(&mutex_b); printf("the chile thread got mutex b,waiting fo mutex a\n"); usleep(500*1000); b++; pthread_mutex_lock(&mutex_a); a+=b++; pthread_mutex_unlock(&mutex_a); pthread_mutex_unlock(&mutex_b); } pthread_exit(NULL); } signed main(){ pthread_mutex_init(&mutex_a,NULL); pthread_mutex_init(&mutex_b,NULL); pthread_t tid; pthread_create(&tid,NULL,run,NULL); //向a鎖加鎖 int x=0; while(x<=INF){ pthread_mutex_lock(&mutex_a); printf("the father thread got mutex a,waiting for mutext b\n"); a++; pthread_mutex_lock(&mutex_b); b+=a++; pthread_mutex_unlock(&mutex_b); pthread_mutex_unlock(&mutex_a); } pthread_join(tid,NULL); pthread_mutex_destroy(&mutex_a); pthread_mutex_destroy(&mutex_b); return 0; } ```
-
條件變量
與互斥鎖不同,條件變量是用來等待而不是用來上鎖的。條件變量用來自動阻塞一個線程,直 到某特殊情況發生爲止。通常條件變量和互斥鎖同時使用。
條件變量使我們可以睡眠等待某種條件出現。條件變量是利用線程間共享的全局變量進行同步 的一種機制,主要包括兩個動作:
-
一個線程等待"條件變量的條件成立"而掛起;
-
另一個線程使 “條件成立”(給出條件成立信號)。
更加清晰的說法就是,條件變量提供了一種線程間的通知機制:當某個共享數據達到某個值的 時候,喚醒等到這個共享數據的線程。
#include <pthread.h> // 初始化條件變量 int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr); // 阻塞等待 int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex); // 超時等待 int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex, const timespec *abstime); // 解除所有線程的阻塞 int pthread_cond_destroy(pthread_cond_t *cond); // 至少喚醒一個等待該條件的線程 int pthread_cond_signal(pthread_cond_t *cond); // 喚醒等待該條件的所有線程 int pthread_cond_broadcast(pthread_cond_t *cond);
-
-