我們先來看一段代碼:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> //創建兩個線程,分別對兩個全變量進行++操作,判斷兩個變量是否相等,不相等打印 int a = 0; int b = 0; // 未初始化 和0初始化的成員放在bbs pthread_mutex_t mutex; void* route() { while(1) //初衷不會打印 { a++; b++; if(a != b) { printf("a =%d, b = %d\n", a, b); a = 0; b = 0; } } } int main() { pthread_t tid1, tid2; pthread_create(&tid1, NULL, route, NULL); pthread_create(&tid2, NULL, route, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; }
這段代碼的運行結果優點出乎我們的預料:
我們預計的結構應該是不會打印的,而這里去打印出了我們意想不到的結果。連相等的數據都打印了出來,為什么會出現這樣的情況呢?
解釋:兩個線程互相搶占CPU資源,一個線程對全局變量做了++操作之后,還沒來得及比較輸出操作,另一個線程搶占CPU,進行比較打印輸出。為了避免這樣的情況,就需要用到下面介紹的互斥鎖。
互斥量(鎖):用於保護關鍵的代碼段,以確保其獨占式的訪問。
1.定義互斥量: pthread_mutex_t mutex;
2.初始化互斥量: pthread_mutex_init(&mutex, NULL); //第二個參數不研究置NULL; //初始化為 1 (僅做記憶)
3.上鎖 pthread_mutex_lock(&mutex); 1->0; 0 等待
4.解鎖 pthread_mutex_unlock(&mutex); 置1 返回
5.銷毀 pthread_mutex_destroy(&mutex);
返回值:若成功返回0,若出錯返回錯誤編號。
說明: 互斥鎖,在多個線程對共享資源進行訪問時,在訪問共享資源前對互斥量進行加鎖,在訪問完再進行解鎖,在互斥量加鎖后其他的線程將阻塞,直到當前的線程訪問完畢並釋放鎖。如果釋放互斥鎖時有多個線程阻塞,所有阻塞線程都會變成可運行狀態,第一個變成可運行狀態的線程可以對互斥量加鎖。這樣就保證了每次只有一個線程訪問共享資源。
至此,我們好像能通過互斥鎖解決上面的問題:
#include <stdio.h> #include <stdlib.h> #include <pthread.h> int a = 0; int b = 0; // 未初始化 和0初始化的成員放在bbs pthread_mutex_t mutex; void* route() { while(1) //初衷不會打印 { pthread_mutex_lock(&mutex); a++; b++; if(a != b) { printf("a =%d, b = %d\n", a, b); a = 0; b = 0; } pthread_mutex_unlock(&mutex); } } int main() { pthread_t tid1, tid2; pthread_mutex_init(&mutex, NULL); pthread_create(&tid1, NULL, route, NULL); pthread_create(&tid2, NULL, route, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_mutex_destroy(&mutex); return 0; }
現有如下場景:線程1和線程2,線程1執行函數A,線程2執行函數B,現只使用一把鎖,分別對A,B函數的執行過程加鎖和解鎖。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> //線程的取消動作發生在加鎖和解鎖過程中時,當發生線程2取消后而沒有進行解鎖時,就會出現線程1將一直阻塞 pthread_mutex_t mutex; void* odd(void* arg) { int i = 1; for(; ; i+=2) { pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); } } void* even(void* arg) { int i = 0; for(; ; i+=2) { pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); } } int main() { pthread_t t1, t2; pthread_mutex_init(&mutex, NULL); pthread_create(&t1, NULL, even, NULL); pthread_create(&t2, NULL, odd, NULL); //pthread_create(&t3, NULL, even, NULL); sleep(3); pthread_cancel(t2); //取消線程2,這個動作可能發生在線程2加鎖之后和解鎖之前 pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_mutex_destroy(&mutex); return 0; }
一種極限情況是:線程2的取消發生在線程2的解鎖之前,那么就會導致因為鎖沒解開,而線程1無法繼續運行。
解決這樣的問題我們可以用到下面的宏函數:
宏: //注冊線程回調函數,可用來防止線程取消后沒有解鎖的問題
void pthread_cleanup_push(void (*routine)(void *), //回調函數
void *arg); //回調函數的參數
//回調函數執行時機
1.pthread_exit
2.pthread_cancel
3.cleanaup_pop參數不為0,當執行到cleaup_pop時,調用回調函數
void pthread_cleanup_pop(int execute);
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> //線程的取消動作發生在加鎖和解鎖過程中時,當發生線程2取消后而沒有進行解鎖時,就會出現線程1將一直阻塞 pthread_mutex_t mutex; void callback(void* arg) //在cancel中進行解鎖 { printf("callback\n"); sleep(1); pthread_mutex_unlock(&mutex); } void* odd(void* arg) { int i = 1; for(; ; i+=2) { pthread_cleanup_push(callback, NULL);//因為調用了cancel函數,從而觸發了回調函數。 pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); pthread_cleanup_pop(0); } } void* even(void* arg) { int i = 0; for(; ; i+=2) { pthread_mutex_lock(&mutex); printf("%d\n", i); pthread_mutex_unlock(&mutex); } } int main() { pthread_t t1, t2; pthread_mutex_init(&mutex, NULL); pthread_create(&t1, NULL, even, NULL); pthread_create(&t2, NULL, odd, NULL); //pthread_create(&t3, NULL, even, NULL); sleep(3); pthread_cancel(t2); //取消線程2,這個動作可能發生在線程2加鎖之后和解鎖之前 //pthread_mutex_unlock(&mutex); 有問題,如果執行even的程序有兩個,而一個取消線程的函數執行時正好t3函數阻塞,就會導致t3和t1同時在執行even pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_mutex_destroy(&mutex); return 0; }
注意:
1.不要銷毀一個已經加鎖的互斥量,銷毀的互斥量確保后面不會再有線程使用。
2.上鎖和解鎖函數要成對的使用
3.選擇合適的鎖的粒度(數量)。如果粒度太粗,就會出現很多線程阻塞等待相同鎖,源自並發性的改善微乎其微。如果鎖的粒度太細,那么太多的鎖的開銷會使系統的性能受到影響,而且代碼會變得相當復雜。
4.加鎖要加最小(范圍)鎖,減少系統負擔
使用互斥鎖一定要注意避免死鎖:《Linux高性能服務器編程》 14.5.3 介紹了兩個互斥量因請求順序產生死鎖問題
如果線程試圖對同一個互斥量加鎖兩次,那么它自身就會陷入死鎖狀態,使用互斥量時,還有其他更不明顯的方式也能產生死鎖。例如,程序中使用多個互斥量時,如果允許一個線程一直占有第一個互斥量,並且在試圖鎖住第二個互斥量時處於阻塞狀態,但是擁有第二個互斥量的線程也在試圖鎖住第一個互斥量,這時就會發生死鎖。因為兩個線程都在相互請求另一個線程擁有的資源,所以這兩個線程都無法向前運行,於是就產生死鎖。
可以通過小心地控制互斥量加鎖的順序來避免死鎖的發生。例如,假設需要對兩個互斥量A和B同時加鎖,如果所有線程總是在對互斥量B加鎖之前鎖住互斥量A,那么使用這兩個互斥量不會產生死鎖(當然在其他資源上仍可能出現死鎖);類似地,如果所有的線程總是在鎖住互斥量A之前鎖住互斥量B,那么也不會發生死鎖。只有在一個線程試圖以與另一個線程相反的順序鎖住互斥量時,才可能出現死鎖。
為了應對死鎖,在實際的編程中除除了加上同步互斥量之外,還可以通過以下三原則來避免寫出死鎖的代碼:
1>短:寫的代碼盡量簡潔
2>平:代碼中沒有復雜的函數調用
3>快:代碼的執行速度盡可能快
自旋鎖: 應用在實時性要求較高的場合(缺點:CPU浪費較大)
pthread_mutex_spin;
pthread_spin_lock() ; //得不到時,進入忙等待,不斷向CPU進行詢問請求
pthread_spin_unlock();
pthread_spin_destroy(pthread_spinlock_t *lock);
pthread_spin_init(pthread_spinlock_t *lock, int pshared);
讀寫鎖(共享-獨占鎖):應用場景---大量的讀操作 較少的寫操作
注意:讀讀共享, 讀寫互斥,寫優先級高(同時到達)
1. pthread_rwlock_t rwlock;//定義
2.int pthread_rwlock_init()//初始化
3.pthread_rwlock_rdlock()//pthread_rwlock_wrlock//讀鎖/寫鎖
4.pthread_rwlock_unlock() // 解鎖
5.int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//銷毀鎖
返回值:成功返回0,出錯返回錯誤編號
說明:不管什么時候要增加一個作業到隊列中或者從隊列中刪除作業,都用寫鎖,
不管何時搜索隊列,首先獲取讀模式下的鎖,允許所有的工作線程並發的搜索隊列。在這樣的情況下只有線程
搜索隊列的頻率遠遠高於增加或刪除作業時,使用讀寫鎖才可能改善性能。
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> //創建8個線程,3個寫線程,5個讀線程 pthread_rwlock_t rwlock; int counter = 0; void* readfunc(void* arg) { int id = *(int*)arg; free(arg); while(1) { pthread_rwlock_rdlock(&rwlock); printf("read thread %d : %d\n", id, counter); pthread_rwlock_unlock(&rwlock); usleep(100000); } } void* writefunc(void* arg) { int id = *(int*)arg; free(arg); while(1) { int t = counter; pthread_rwlock_wrlock(&rwlock); printf("write thread %d : t= %d, %d\n", id, t, ++counter); pthread_rwlock_unlock(&rwlock); usleep(100000); } } int main() { pthread_t tid[8]; pthread_rwlock_init(&rwlock, NULL); int i = 0; for(i = 0; i < 3; i++) { int* p =(int*) malloc(sizeof(int)); *p = i; pthread_create(&tid[i], NULL, writefunc, (void*)p); } for(i = 0; i < 5; i++) { int* p = (int*)malloc(sizeof(int)); *p = i; pthread_create(&tid[3+i], NULL, readfunc, (void*)p); } for(i = 0; i < 8; i++) { pthread_join(tid[i], NULL); } pthread_rwlock_destroy(&rwlock); return 0; }
條件變量: 如果說互斥鎖是用於同步線程對共享數據的訪問的化,那么條件變量這是用於在線程之間同步共享數據的值。條件變量提供了一種線程間的通信機制:當某個共享數據達到某個值的時候,喚醒等待這個共享數據的線程
1.定義條件變量 pthread_cond_t cond;
2.初始化 pthread_cond_init(&cond, NULL);
3.等待條件 pthread_cond_wait(&cond, &mutex);
mutex :如果沒有在互斥環境,形同虛設
在互斥環境下:wait函數將mutex置1,wait返回,mutex恢復成原來的值
4.修改條件 pthread_cond_signal(&cond);
5.銷毀條件 pthread_cond_destroy(&cond);
規范寫法: pthread_mutex_lock(); while(條件不滿足) pthread_cond_wait(); //為什么會使用while? //因為pthread_cond_wait是阻塞函數,可能被信號打斷而返回(喚醒),返回后從當前位置向下執行, 被信號打斷而返回(喚醒),即為假喚醒,繼續阻塞 pthread_mutex_unlock(); pthread_mutex_lock(); pthread_cond_signal(); //信號通知 ---- 如果沒有線程在等待,信號會被丟棄(不會保存起來)。 pthread_mutex_unlock();
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> //創建兩個線程一個wait print,一個signal sleep() pthread_cond_t cond; pthread_mutex_t mutex; void* f1(void* arg) { while(1) { pthread_cond_wait(&cond, &mutex); printf("running!\n"); } } void* f2(void* arg) { while(1) { sleep(1); pthread_cond_signal(&cond); } } int main() { pthread_t tid1, tid2; pthread_cond_init(&cond, NULL); pthread_mutex_init(&mutex, NULL); pthread_create(&tid1, NULL, f1, NULL); pthread_create(&tid2, NULL, f2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); return 0; }
System V //基於內核持續性
信號量: POSIX //基於文件持續性的信號量
1.定義信號量: sem_t sem;
2,初始化信號量: sem_init(sem_t* sem,
int shared, //0表示進程內有多少個線程使用
int val); //信號量初值
3.PV操作 int sem_wait(sem_t* sem); //sem--;如果小於0,阻塞 P操作
int sem_post(sem_t* sem); //sem++; V操作
4.銷毀 sem_destroy(sem_t* sem);
信號量實現生產者消費者模型:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #include <semaphore.h> //倉庫中裝產品編號,沒裝產品的位置,置為-1,裝了的地方置為產品的編號 #define PRO_COUNT 3 #define CON_COUNT 2 #define BUFSIZE 5 sem_t sem_full; //標識可生產的產品個數 sem_t sem_empty; //表示可消費的產品個數 pthread_mutex_t mutex; //互斥量 int num = 0; //產品編號 int buf[BUFSIZE]; //倉庫 int wr_idx; //寫索引 int rd_idx; //讀索引 void* pro(void* arg) { int i = 0; int id = *(int*)arg; free(arg); while(1) { sem_wait(&sem_full); //先判斷倉庫是否滿 pthread_mutex_lock(&mutex); //互斥的訪問具體的倉庫的空閑位置 printf("%d生產者開始生產%d\n", id, num); for(i = 0; i < BUFSIZE; i++) { printf("\tbuf[%d]=%d", i, buf[i]); if(i == wr_idx) { printf("<====="); } printf("\n"); } buf[wr_idx] = num++; //存放產品 wr_idx = (wr_idx + 1) % BUFSIZE; printf("%d生產者結束生產\n", id); pthread_mutex_unlock(&mutex); sem_post(&sem_empty); sleep(rand()%3); } } void* con(void* arg) { int i = 0; int id = *(int*)arg; free(arg); while(1) { sem_wait(&sem_empty); pthread_mutex_lock(&mutex); printf("%d消費者開始消費%d\n", id, num); for(i = 0; i < BUFSIZE; i++) { printf("buf[%d]=%d", i, buf[i]); if(i == rd_idx) { printf("=====>"); } printf("\n"); } int r = buf[rd_idx]; buf[rd_idx] = -1; rd_idx = (rd_idx+1)%BUFSIZE; sleep(rand()%4); printf("%d\n消費者消費完%d\n", id, r); pthread_mutex_unlock(&mutex); sem_post(&sem_full); sleep(rand()%2); } } int main() { pthread_t tid[PRO_COUNT+CON_COUNT]; pthread_mutex_init(&mutex, NULL); //初始化 sem_init(&sem_empty, 0, 0); sem_init(&sem_full, 0, BUFSIZE); srand(getpid()); int i = 0; for(i = 0; i < BUFSIZE; i++) //初始化倉庫 -1表示沒有品 buf[i] = -1; for(i = 0; i < PRO_COUNT; i++) //產生生產者 { int *p = (int*)malloc(sizeof(int)); *p = i; pthread_create(&tid[i], NULL, pro, p); } for(i = 0; i < CON_COUNT; i++) { int *p = (int*)malloc(sizeof(int)); *p = i; pthread_create(&tid[i+CON_COUNT], NULL, con, p); } for(i = 0; i < PRO_COUNT + CON_COUNT; i++) { pthread_join(tid[i], NULL); } pthread_mutex_destroy(&mutex); //銷毀 sem_destroy(&sem_empty); sem_destroy(&sem_full); return 0; }
拓展學習:
樂觀鎖和悲觀鎖?
樂觀鎖:
在關系數據庫管理系統里,樂觀並發控制(又名”樂觀鎖”,Optimistic Concurrency Control,縮寫”OCC”)是一種並發控制的方法。它假設多用戶並發的事務在處理時不會彼此互相影響,各事務能夠在不產生鎖的情況下處理各自影響的那部分數據。在提交數據更新之前,每個事務會先檢查在該事務讀取數據后,有沒有其他事務又修改了該數據。如果其他事務有更新的話,正在提交的事務會進行回滾。
樂觀並發控制的事務包括以下階段:
1. 讀取:事務將數據讀入緩存,這時系統會給事務分派一個時間戳。
2. 校驗:事務執行完畢后,進行提交。這時同步校驗所有事務,如果事務所讀取的數據在讀取之后又被其他事務修改,則產生沖突,事務被中斷回滾。
3. 寫入:通過校驗階段后,將更新的數據寫入數據庫。
優點和不足:
樂觀並發控制相信事務之間的數據競爭(data race)的概率是比較小的,因此盡可能直接做下去,直到提交的時候才去鎖定,所以不會產生任何鎖和死鎖。但如果直接簡單這么做,還是有可能會遇到不可預期的結果,例如兩個事務都讀取了數據庫的某一行,經過修改以后寫回數據庫,這時就遇到了問題。
悲觀鎖:
在關系數據庫管理系統里,悲觀並發控制(又名”悲觀鎖”,Pessimistic Concurrency Control,縮寫”PCC”)是一種並發控制的方法。它可以阻止一個事務以影響其他用戶的方式來修改數據。如果一個事務執行的操作讀某行數據應用了鎖,那只有當這個事務把鎖釋放,其他事務才能夠執行與該鎖沖突的操作。
優點和不足:悲觀並發控制實際上是“先取鎖再訪問”的保守策略,為數據處理的安全提供了保證。但是在效率方面,處理加鎖的機制會讓數據庫產生額外的開銷,還有增加產生死鎖的機會;另外,在只讀型事務處理中由於不會產生沖突,也沒必要使用鎖,這樣做只能增加系統負載;還有會降低了並行性,一個事務如果鎖定了某行數據,其他事務就必須等待該事務處理完才可以處理那行數
系統最多能夠創建多少個線程? (一般以實測為准,但根據每次開辟的棧的大小不同,測試結果也會不同)。
一個是直接在命令行查看 cat /proc/sys/kernel/threads-max 我的電腦顯示是 7572
另一個是自己計算 用戶空間大小3G 即是3072M/8M棧空間 = 380
第三個寫程序: 跑到32754(理論值 32768)
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> //創建線程 void* foo(void* arg) { } int main() { int count = 0; pthread_t thread; while(1) { if(pthread_create(&thread, NULL, foo, NULL) != 0) return 1; count++; printf("MAX = %d\n", count); } return 0; }
轉自:(9條消息) 線程的幾種鎖及基本操作_czf的編程工坊-CSDN博客_線程鎖