線程的概念
線程:light weight process首尼,輕量級的進程价涝,Linux環(huán)境下本質(zhì)上仍是進程掀抹。和進程的區(qū)別是蹬竖,進程有獨立的地址空間和PCB型凳,而線程有PCB但是沒有獨立的地址空間丈冬。在Linux環(huán)境下,線程是最小的執(zhí)行單位甘畅,而進程是最小分配資源單位埂蕊。
線程共享資源:
- 文件描述符表。
- 每種信號的處理方式疏唾。
- 當(dāng)前工作目錄蓄氧。
- 用戶ID和組ID。
- 內(nèi)存地址空間(.text/.data/.bss/heap/共享庫)槐脏。
線程非共享資源:
- 線程id喉童。
- 處理器現(xiàn)場和棧指針(內(nèi)核棧)。
- 獨立的椂偬欤空間(用戶空間棧)堂氯。
- errno變量。
- 信號屏蔽字牌废。
- 進程調(diào)度優(yōu)先級咽白。
線程的優(yōu)缺點
優(yōu)點:1,提高程序并發(fā)性鸟缕。2晶框,開銷小。3懂从,數(shù)據(jù)通信授段,共享數(shù)據(jù)方便。
缺點:1莫绣,庫函數(shù)畴蒲,穩(wěn)定性稍低。2对室,調(diào)試模燥、編寫困難,gdb不支持掩宜。3蔫骂,對信號支持不好。
線程的創(chuàng)建和回收
#include<pthread.h>
#include<stdlib.h>
#include<string.h>
#include<stdio.h>
struct person{
char name[20];
int age;
};
typedef struct person* Person;
void *func(void *arg){
Person p = (Person)arg;
printf("name = %s,age = %d\n",p->name,p->age);
Person ret = (Person)malloc(sizeof(struct person));
strcpy(ret->name,p->name);
ret->age = p->age+1;
return (void *)ret;
}
int main(int argc, char const *argv[])
{
pthread_t tid;
Person p = (Person)malloc(sizeof(struct person));
strcpy(p->name,"Jack");
p->age = 34;
pthread_create(&tid,NULL,func,(void *)p);
Person ret;
pthread_join(tid,(void**)&ret);
printf("name = %s,age = %d\n",ret->name,ret->age);
free(ret);
system("pause");
return 0;
}
由于線程共享的東西比較多牺汤,如果不使用同步辽旋,線程之間是競爭的關(guān)系,所以,我們無法預(yù)測線程之間的執(zhí)行順序补胚,并且码耐,在線程執(zhí)行的過程中,也可能切換線程溶其。下面是一個例子:
#include<stdio.h>
#include<pthread.h>
int i = 0;
void *func(void *arg){
while(1){
i++;
i++;
}
}
int main(int argc, char const *argv[])
{
pthread_t tid;
pthread_create(&tid,NULL,func,(void *)NULL);
while(1){
i++;
i++;
if(i % 2 != 0){
printf("error! i = %d is odd\n",i);
break;
}
}
return 0;
}
這個程序每次運行的結(jié)果都不一樣骚腥,這就是與時間有關(guān)的錯誤。產(chǎn)生這種錯誤有三個條件:
- 共享數(shù)據(jù)瓶逃。
- 多個對象競爭束铭。
- 沒有合理的同步機制。
要解決這種問題厢绝,需要使用同步的機制契沫。
線程使用注意事項
- 需要主線程退出其他線程不退出,主線程應(yīng)調(diào)用pthread_exit方法昔汉。
- 要避免僵尸線程懈万,使用pthread_join顯示回收,或者使用pthread_detach分離線程或者在pthread_create中指定分離屬性靶病。
- malloc和mmap申請的內(nèi)存可以被其他線程釋放钞速。
- 應(yīng)避免在多線程中調(diào)用fork,除非馬上exec嫡秕,子進程中只有調(diào)用fork的線程存在,其他線程在子進程中均pthread_exit苹威。
互斥量
Linux中提供一把互斥鎖mutex(也稱之為互斥量)昆咽。每個線程在對資源操作前都嘗試先加鎖,成功加鎖會才能操作牙甫,操作結(jié)束解鎖掷酗。資源還是共享的,線程間也還是競爭的窟哺,但通過鎖泻轰,就將資源的訪問變成互斥操作,而后與時間有關(guān)的錯誤也不會產(chǎn)生了且轨「∩互斥鎖只有一把,所以同一個時刻只有一個線程擁有這把鎖旋奢。
互斥鎖實質(zhì)上是操作系統(tǒng)提供的一把”建議鎖“(又稱”協(xié)同鎖“)泳挥,建議程序中有多線程訪問共享資源的時候使用該機制,但至朗,并沒有強制限定屉符。因此,即使有了mutex,如果有線程不按規(guī)則來訪問數(shù)據(jù)矗钟,依然會造成數(shù)據(jù)混亂唆香。如,當(dāng)A線程對某個全局變量加鎖訪問吨艇,B在訪問之前嘗試加鎖躬它,拿不到鎖,B阻塞秸应。C線程不去加鎖虑凛,而直接訪問該全局變量,依然能夠訪問软啼,但會出現(xiàn)數(shù)據(jù)混亂桑谍。
mutex有以下的一些函數(shù):
/* 獲得一個初始化好的鎖 */
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
/* 初始化鎖 */
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
/* 加鎖,阻塞 */
int pthread_mutex_lock(pthread_mutex_t *mutex);
/* 加鎖祸挪,不阻塞 */
int pthread_mutex_trylock(pthread_mutex_t *mutex);
/* 解鎖 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/* 釋放鎖 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);
上面的程序可以改成下面的形式:
#include<stdio.h>
#include<pthread.h>
pthread_mutex_t mutex;
int i = 0;
void *func(void *arg){
while(1){
pthread_mutex_lock(&mutex);
i++;
i++;
pthread_mutex_unlock(&mutex);
}
}
int main(int argc, char const *argv[])
{
pthread_t tid;
pthread_mutex_init(&mutex,NULL);
pthread_create(&tid,NULL,func,(void *)NULL);
while(1){
pthread_mutex_lock(&mutex);
i++;
i++;
if(i % 2 != 0){
printf("error! i = %d is odd\n",i);
break;
}
pthread_mutex_unlock(&mutex);
}
pthread_mutex_destroy(&mutex);
return 0;
}
在做這個demo的時候锣披,犯了一個錯誤,把unlock寫到了檢查是否為奇數(shù)之前贿条,也會出現(xiàn)上面那種現(xiàn)象雹仿,原因在于,有可能恰好在檢查前失去CPU整以,然后執(zhí)行一次i++之后又得到了CPU(此時雖然已經(jīng)上鎖了胧辽,但是不會去檢查,會繼續(xù)執(zhí)行)公黑。
死鎖及其解決方案
一個線程可以通過某種形式的加鎖機制來防止別的線程在互斥還沒有釋放的時候就訪問這個資源邑商。值得注意的是,加鎖是阻塞的凡蚜,所以可能會出現(xiàn)這種情況:某個線程在等待另一個線程人断,而后者也在等待別的線程,這樣一直下去朝蜘,直到這個鏈條上的線程又在等待第一個線程釋放鎖恶迈。這得到一個任務(wù)之間互相等待的連續(xù)循環(huán),沒有哪個線程能夠繼續(xù)谱醇,這被稱為死鎖暇仲。最簡單的情況就是自己等自己,如下面的這個程序:
#include<stdio.h>
#include<pthread.h>
int main(int argc, char const *argv[])
{
pthread_mutex_t pit = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&pit);
printf("Lock again\n");
pthread_mutex_lock(&pit);
return 0;
}
再次上鎖的時候副渴,需要先釋放鎖熔吗,但是線程本身是無法釋放鎖的,所以就會進入死循環(huán)佳晶。
死鎖的產(chǎn)生需要同時滿足以下4個條件:
- 互斥條件桅狠。線程使用的資源中至少有一個是不能共享的。
- 至少有一個線程它必須持有一個資源且正在等待獲取一個當(dāng)前被別的線程持有的資源。
- 資源不能被搶占中跌,只能等待其他線程釋放資源咨堤。
- 必須有循環(huán)等待。
這些條件需要全部滿足才會產(chǎn)生死鎖漩符,所以一喘,為了避免死鎖,只需要破壞其中一個條件即可嗜暴。在程序中凸克,防止死鎖最容易的方法是破壞第4個條件。也就是說闷沥,我們要注意資源的獲取順序萎战,最好是一致的。如:A獲取順序1舆逃,2蚂维,3;B順序也是1,2,3路狮,則不會發(fā)生死鎖虫啥。而如果B的順序是3,2,1,則容易出現(xiàn)死鎖奄妨。因為后者會出現(xiàn)互相等待的情況涂籽。下面以一個哲學(xué)家吃飯的例子來說明這個問題:
#include<stdio.h>
#include<pthread.h>
pthread_mutex_t mutex[5];
void* philosopher(void *arg){
int id = (int)arg;
int left,right;
if(id<4){
left = id;
right = id + 1;
}else if(id == 4){
/* 這里會出現(xiàn)死鎖,如果要避免死鎖砸抛,交換left和right的值即可 */
left = id;
right = 0;
}
while(1){
/* 先拿左邊的又活,再拿右邊的 */
pthread_mutex_lock(&mutex[left]);
pthread_mutex_lock(&mutex[right]);
printf("philosopher %d eating\n",id);
pthread_mutex_unlock(&mutex[left]);
pthread_mutex_unlock(&mutex[right]);
}
}
int main(int argc, char const *argv[])
{
int i;
pthread_t th[5];
for(i=0;i<5;i++){
mutex[i] = PTHREAD_MUTEX_INITIALIZER;
}
for(i=0;i<5;i++){
pthread_create(&th[i],NULL,philosopher,(void *)i);
}
for(i=0;i<5;i++){
pthread_join(th[i],NULL);
}
for(i=0;i<5;i++){
pthread_mutex_destroy(&mutex[i]);
}
return 0;
}
條件變量及生產(chǎn)者消費者模型
條件變量本身不是鎖!但它也可以造成線程阻塞锰悼。通常與互斥鎖配合使用,給多線程提供一個會和的場所团赏。
/* 獲得一個初始化好的鎖 */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
/* 初始化條件變量 */
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
/* 喚醒一個等待該條件變量的線程 */
int pthread_cond_signal(pthread_cond_t *cond);
/* 喚醒全部等待的條件變量 */
int pthread_cond_broadcast(pthread_cond_t *cond);
/* 等待一個條件變量箕般,并且將mutex解鎖,喚醒后將mutex加鎖 */
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
/* 和wait差不多舔清,只不過到了絕對時間點會直接喚醒 */
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
/* 釋放條件變量 */
int pthread_cond_destroy(pthread_cond_t *cond);
線程同步典型的案例即為生產(chǎn)者消費者模型丝里,而借助條件變量來實現(xiàn)這一模型,是比較常見的一種方法体谒。假定有兩個線程杯聚,一個模擬生產(chǎn)者行為,一個模擬消費者行為抒痒。兩個線程操作一個共享資源幌绍,生產(chǎn)者向其中添加產(chǎn)品,消費者從中消費產(chǎn)品。具體的代碼如下:
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
int i = -1;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_full = PTHREAD_COND_INITIALIZER;
void *cfunc(void *arg){
int id = (int)arg;
while(1){
/* 要訪問i傀广,必須先加鎖 */
pthread_mutex_lock(&lock);
while(i == -1){
/* 消費者等待滿 */
pthread_cond_wait(&cond_full,&lock);
}
printf("consumer %d consume %d\n",id,i);
i = -1;
/* 解鎖之后通知生產(chǎn)者空了 */
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond_empty);
}
}
void *pfunc(void *arg){
int id = 0;
while(1){
/* 要訪問i颁独,必須加鎖 */
pthread_mutex_lock(&lock);
while(i != -1){
/* 生產(chǎn)者等待空 */
pthread_cond_wait(&cond_empty,&lock);
}
printf("producer procduce %d\n",++id);
i = id;
/* 解鎖后通知消費者滿了 */
sleep(1);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond_full);
}
}
int main(int argc, char const *argv[])
{
pthread_t cid1,cid2,pid;
pthread_create(&cid1,NULL,cfunc,(void *)1);
pthread_create(&cid2,NULL,cfunc,(void *)2);
pthread_create(&pid,NULL,pfunc,NULL);
pthread_join(cid1,NULL);
pthread_join(cid2,NULL);
pthread_join(pid,NULL);
pthread_cond_destroy(&cond_empty);
pthread_cond_destroy(&cond_full);
pthread_mutex_destroy(&lock);
return 0;
}
相較于mutex而言,條件變量可以減少競爭伪冰。如直接使用mutex誓酒,除了生產(chǎn)者,消費者之間要競爭互斥量以外贮聂,消費者之間也需要競爭互斥量靠柑,但如果沒有產(chǎn)品,消費者之間競爭互斥鎖是無意義的吓懈。有了條件變量機制以后歼冰,只有生產(chǎn)者完成生產(chǎn),才會引起消費者之間的競爭骄瓣,提高了程序效率停巷。在我看來,阻塞相當(dāng)于while循環(huán)榕栏,而wait是sleep畔勤,喚醒之后才會繼續(xù)執(zhí)行。
在JAVA中有一個CountDownLatch類扒磁,可以用來同步一個或多個線程庆揪,強制它們等待由其他線程執(zhí)行的一組操作,這里我用C語言簡單實現(xiàn)了一個類似的場景妨托,就是5個線程同時準(zhǔn)備好了缸榛,才可以執(zhí)行后面的操作。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
int n;
void *func(void *arg){
int id = (int)arg;
printf("thread %d preparing\n",id);
sleep(id);
pthread_mutex_lock(&mutex);
n--;
if(n==0){
pthread_cond_broadcast(&cond);
}else{
pthread_cond_wait(&cond,&mutex);
}
pthread_mutex_unlock(&mutex);
printf("thread %d working\n",id);
return (void *)NULL;
}
int main(int argc, char const *argv[])
{
mutex = PTHREAD_MUTEX_INITIALIZER;
cond = PTHREAD_COND_INITIALIZER;
n = 5;
pthread_t pt[5];
int i;
for(i=0;i<5;i++){
pthread_create(&pt[i],NULL,func,(void *)(i+1));
pthread_detach(pt[i]);
}
pthread_exit(NULL);
return 0;
}
信號量
信號量是進化版的互斥鎖兰伤,也就是從1變成了N内颗。由于互斥鎖的粒度較大,如果我們希望在多個線程間對某一對象的部分數(shù)據(jù)進行共享敦腔,使用互斥鎖是沒有辦法實現(xiàn)的均澳,只能將真?zhèn)€數(shù)據(jù)對象鎖住,這樣雖然達到了多線程操作共享數(shù)據(jù)正確性的目的符衔,卻無形中導(dǎo)致線程的并發(fā)性下降找前。線程從并行執(zhí)行,變成了串行執(zhí)行判族。與字節(jié)使用單線程無異躺盛。信號量,是相對折中的一種處理方式形帮,既能保證同步槽惫,數(shù)據(jù)不混亂周叮,又能提高線程并發(fā)。信號量常用的有以下一些函數(shù):
/* 初始化信號量躯枢,值為value则吟,可以設(shè)定共享還是非共享 */
int sem_init(sem_t *sem, int pshared, unsigned int value);
/* 釋放鎖 */
int sem_destroy(sem_t *sem);
/* 信號量減1,如果小于0锄蹂,就阻塞等待 */
int sem_wait(sem_t *sem);
/* 和wait類似氓仲,不阻塞 */
int sem_trywait(sem_t *sem);
/* 和wait類似,到了時間自動解除阻塞 */
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
/* 使信號量加1得糜,如果有等待sem的線程敬扛,就喚醒這些線程 */
int sem_post(sem_t *sem);
下面是一個使用信號量實現(xiàn)生產(chǎn)者消費者模型的例子:
#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
#define MAX 100
sem_t sp,sc;
int arr[5];
void *produce(void *arg){
int i = 0;
int n = 1;
while(n<=MAX){
sem_wait(&sp);/* 拿空位置 */
arr[i] = n++;
i = (i+1)%5;
sem_post(&sc);/* 產(chǎn)品數(shù)目加1 */
}
return (void *)NULL;
}
void *consume(void *arg){
int i = 0;
int n = 0;
while(n<MAX){
sem_wait(&sc);
n = arr[i];
i = (i+1)%5;
printf("consume %d\n",n);
sem_post(&sp);
}
return (void *)NULL;
}
int main(int argc, char const *argv[])
{
sem_init(&sp,0,5);/* 開始有5個空位置 */
sem_init(&sc,0,0);/* 開始一個產(chǎn)品也沒有 */
pthread_t pt[2];
pthread_create(&pt[0],NULL,produce,NULL);
pthread_create(&pt[1],NULL,consume,NULL);
pthread_join(pt[0],NULL);
pthread_join(pt[1],NULL);
sem_destroy(&sp);
sem_destroy(&sc);
return 0;
}
讀寫鎖
與互斥量類似,但讀寫鎖允許更高的并行性朝抖。讀寫鎖是一把鎖啥箭,只不過讀寫鎖有兩個不同的加鎖方式:可以以讀的方式加鎖,也可以以寫的方式加鎖治宣。讀寫鎖有以下的特性:
- 讀寫鎖是”寫模式加鎖“時急侥,解鎖前,所有對該鎖加鎖的線程都會被阻塞侮邀。
- 讀寫鎖是“讀模式加鎖”時坏怪,如果線程以讀模式對其加鎖會成功;如果以寫模式進行加鎖會阻塞绊茧。
- 讀寫鎖是“讀模式加鎖”時铝宵,既有視圖以寫模式加鎖的線程,也有試圖以讀模式加鎖的線程华畏,那么讀寫鎖會阻塞隨后的讀模式鎖鹏秋。讀鎖、寫鎖并行阻塞亡笑,寫鎖優(yōu)先級高侣夷。
總結(jié)起來就一句話:寫?yīng)氄迹x共享仑乌,寫鎖優(yōu)先級高百拓。
讀寫鎖有以下的一些常用函數(shù):
/* 初始化讀寫鎖 */
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
/* 銷毀鎖 */
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
/* 以讀的方式加鎖,阻塞 */
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
/* 以讀的方式加鎖,不阻塞 */
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
/* 以寫的方式加鎖绝骚,阻塞 */
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
/* 以寫的方式加鎖,不阻塞 */
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
/* 解鎖 */
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
下面以一個簡單的例子來說明讀寫鎖的特性:
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
pthread_rwlock_t rwlock;
int n;
void *reader(void *arg){
int id = (int)arg;
while(n <= 100){
pthread_rwlock_rdlock(&rwlock);
printf("reading thread %d reading data %d\n",id,n);
sleep(1);
printf("reading thread %d finish reading\n",id);
pthread_rwlock_unlock(&rwlock);
}
return NULL;
}
void *writer(void *arg){
int id = (int)arg;
while(n <= 100){
pthread_rwlock_wrlock(&rwlock);
printf("writing thread %d writing data %d\n",id,++n);
sleep(1);
printf("writing thread %d finish reading\n",id);
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
return NULL;
}
int main(int argc, char const *argv[])
{
n = 0;
pthread_rwlock_init(&rwlock,NULL);
int i;
pthread_t tid[3];
for(i=0;i<2;i++){
pthread_create(&tid[i],NULL,reader,(void *)(i+1));
}
pthread_create(&tid[2],NULL,writer,(void *)3);
for(i=0;i<3;i++){
pthread_join(tid[i],NULL);
}
return 0;
}
可以看到的現(xiàn)象是祠够,讀模式加鎖時压汪,其他讀線程可以進入,寫線程加鎖的時候古瓤,所有的線程都阻塞止剖。值得注意的是腺阳,如果寫線程后面不sleep一秒的話,就寫線程一個人自己玩穿香,因為解鎖之后馬上加鎖亭引,寫線程很容易搶到這把鎖。