1. 在並發情況下,指令執行的先后順序由內核決定。同一個線程內部,指令按照先后順序執行,但不同線程之間的指令很難說清楚是哪一個先執行。如果運行的結果依賴於多線程執行的順序,那么就會形成競爭條件,每次運行的結果可能會不同,所以應該盡量避免競爭條件的形成。
2. 最常見的解決競爭條件的方法是將原先分離的兩個指令構成一個不可分割的原子操作,其他任務就不能插入到原子操作中!
3. 對多線程來說,同步指的是在一定時間內只允許某一個線程訪問某個資源,而在此時間內,不允許其他線程訪問該資源!
4. 線程同步的常見方法:互斥鎖,條件變量,讀寫鎖,信號量
一、互斥鎖(互斥量)
互斥鎖是一種特殊的變量,有上鎖(lock)和解鎖(unlock)兩種狀態。
當處於解鎖狀態時,線程想獲取該互斥鎖,就可以獲取不被阻塞,互斥鎖變為鎖定狀態;
當處於鎖定狀態時,線程獲取互斥鎖被阻塞,並加入到這個互斥鎖的等待隊列中。
互斥鎖有點像打印機,空閑時你可以打印;別人在打印時,你就需要排隊等待打印機空閑。
1.1 創建並初始化一個互斥鎖
使用 pthread_mutex_t 類型的變量來表示互斥鎖。在使用之前,必須對其進行初始化。
靜態初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
動態初始化:pthread_mutex_init 函數
一般我們都使用靜態初始化。理由:靜態初始化通常比 pthread_mutex_init 更有效,而且可以在定義為全局變量時即完成初始化,這樣可以保證在任何線程開始執行之前,初始化既已完成。
1.2 互斥鎖相關屬性及分類
//初始化互斥鎖屬性 pthread_mutexattr_init(pthread_mutexattr_t attr); //銷毀互斥鎖屬性 pthread_mutexattr_destroy(pthread_mutexattr_t attr); //用於獲取互斥鎖屬性 int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr , int *restrict pshared); //用於設置互斥鎖屬性 int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr , int pshared);
attr表示互斥鎖的屬性,pshared表示互斥鎖的共享屬性,有兩種取值:
1)PTHREAD_PROCESS_PRIVATE:鎖只能用於一個進程內部的兩個線程進行互斥(默認情況)
2)PTHREAD_PROCESS_SHARED:鎖可用於兩個不同進程中的線程進行互斥,使用時還需要在進程共享內存中分配互斥鎖,然后該互斥鎖指定屬性就可以了。
1.3 互斥鎖常用函數
// 銷毀互斥鎖 int pthread_mutex_destroy(pthread_mutex_t *mutex); // 對互斥鎖的鎖定 int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); // 對互斥鎖的解鎖 int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_lock函數會調用這個函數線程一直阻塞到互斥鎖可用為止,而pthread_mutex_trylock會立即返回
二、條件變量
用處:當線程在等待滿足某些條件時使線程進入睡眠狀態,一旦條件滿足,就喚醒線程。由於涉及到共享數據,因此條件變量是結合互斥鎖來使用的。使用 pthread_cond_t 來表示條件變量。
2.1 相關函數
1)創建
靜態初始化:pthread_cond_t convar = PTHREAD_COND_INITIALIZER
動態初始化:int pthread_cond_init(&condvar, NULL)
2) 銷毀
int pthread_cond_destroy(&condvar)
3)等待
條件等待:int pthread_cond_wait(&condvar, &mutex)
計時等待:int pthread_cond_timewait(&condvar, &mutex, time)
1. 計時等待如果在給定時刻前條件沒有被滿足,則返回 ETIMEOUT,結束等待
2. 無論哪種等待方式,都必須有一個互斥鎖配合,以方式多個線程同時請求pthread_cond_wait形成競爭條件。也就是說,在使用 pthread_cond_wait 之前,必須使用互斥鎖加鎖(pthread_mutex_lock);pthread_cond_timewait 同理。
4)喚醒
喚醒一個等待線程:pthread_cond_signal(&condvar)
喚醒所有等待線程:pthread_cond_broadcast(&cond)
重要的是,pthread_cond_signal 不會存在驚群效應,也就是它只喚醒一個等待線程,不會給所有線程發信號喚醒他們,然后要求他們自己去爭搶資源!pthread_cond_signal 會根據等待線程的優先級和等待時間來確定喚醒哪一個等待線程!
2.2 條件變量應用實例------condition_variables.c
#include <stdio.h> #include <pthread.h> int i = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t condvar = PTHREAD_COND_INITIALIZER; void *threadfunc(void *pvoid) { while (1) { pthread_mutex_lock(&mutex); if (i < 200) { i++; pthread_cond_signal(&condvar); /**< 子線程喚醒主線程 */ pthread_mutex_unlock(&mutex); } else { pthread_mutex_unlock(&mutex); break; } } return NULL; } int main() { pthread_t tid; pthread_create(&tid, NULL, &threadfunc, NULL); pthread_mutex_lock(&mutex); while (i < 100) { pthread_cond_wait(&condvar, &mutex); } printf("i = %d\n", i); pthread_mutex_unlock(&mutex); pthread_join(tid, NULL); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&condvar); return 0; }
該實例中,主線程監視全局變量 i 的值,如果 i 小於100,則等待。子線程遞增 i 的值,直到 i 等於200。主線程直 i 大於或等於100之后,才能繼續執行。 運行結果可能是 i = 100,也可能是i = xxx,其中xxx是一個100到200之間的數。子線程每次喚醒條件變量並釋放互斥鎖之后,將於主線程一同競爭互斥鎖。當 i 大於100時,如果主線程獲得互斥鎖,就會顯示 i 的值。也就是說,等待條件變量的線程在被喚醒時,並不自動獲得互斥鎖。
編譯代碼命令:
gcc -o condition_variables condition_variables.c -pthread
三、讀寫鎖
讀寫鎖與互斥鎖類型,也叫共享互斥鎖。互斥鎖有上鎖(lock)和解鎖(unlock)兩種狀態,而且一次只有一個線程可以對其加鎖。讀寫鎖可以有三種狀態:讀模式加鎖,寫模式加鎖,不加鎖。
一次只有一個線程可以占有寫模式的讀寫鎖,但是多個線程可以同時占有讀模式的讀寫鎖(允許多個線程讀但只允許一個線程寫)
讀寫鎖的特點:
如果有其他線程讀數據,則允許其他線程執行讀操作,但不允許寫操作
如果有其他線程寫數據,則其他線程都不允許讀、寫操作
讀寫鎖的規則:
如果某線程申請了讀鎖,其他線程可以再申請讀鎖,但不能申請寫鎖
如果與其他線程申請了寫鎖,則其他線程不能申請讀鎖,也不能申請寫鎖
讀寫鎖適合於對數據結構的讀次數比寫次數多得多的情況
#include <pthread.h> // 初始化讀寫鎖 int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); // 申請讀鎖 int pthread_rwlock_rdlock(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); // 銷毀讀寫鎖 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
實例:
// 一個使用讀寫鎖來實現 4 個線程讀寫一段數據是實例。 // 在此示例程序中,共創建了 4 個線程, // 其中兩個線程用來寫入數據,兩個線程用來讀取數據 #include <stdio.h> #include <unistd.h> #include <pthread.h> pthread_rwlock_t rwlock; //讀寫鎖 int num = 1; //讀操作,其他線程允許讀操作,卻不允許寫操作 void *fun1(void *arg) { while(1) { pthread_rwlock_rdlock(&rwlock); printf("first read num == %d\n", num); pthread_rwlock_unlock(&rwlock); sleep(1); } } //讀操作,其他線程允許讀操作,卻不允許寫操作 void *fun2(void *arg) { while(1) { pthread_rwlock_rdlock(&rwlock); printf("second read num == %d\n", num); pthread_rwlock_unlock(&rwlock); sleep(2); } } //寫操作,其它線程都不允許讀或寫操作 void *fun3(void *arg) { while(1) { pthread_rwlock_wrlock(&rwlock); num++; printf("write thread first\n"); pthread_rwlock_unlock(&rwlock); sleep(2); } } //寫操作,其它線程都不允許讀或寫操作 void *fun4(void *arg) { while(1) { pthread_rwlock_wrlock(&rwlock); num++; printf("write thread second\n"); pthread_rwlock_unlock(&rwlock); sleep(1); } } int main() { pthread_t ptd1, ptd2, ptd3, ptd4; pthread_rwlock_init(&rwlock, NULL);//初始化一個讀寫鎖 //創建線程 pthread_create(&ptd1, NULL, fun1, NULL); pthread_create(&ptd2, NULL, fun2, NULL); pthread_create(&ptd3, NULL, fun3, NULL); pthread_create(&ptd4, NULL, fun4, NULL); //等待線程結束,回收其資源 pthread_join(ptd1, NULL); pthread_join(ptd2, NULL); pthread_join(ptd3, NULL); pthread_join(ptd4, NULL); pthread_rwlock_destroy(&rwlock);//銷毀讀寫鎖 return 0; }
四、信號量
信號量廣泛用於進程或線程間的同步或互斥,信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。
根據信號量的值來判斷是否對公共資源具有訪問權限,當信號量的值大於0時,可以訪問,否則將阻塞。帶有兩個原子操作 P 和 V,一次 P 操作使信號量減1,一次 V 操作使信號量加 1。
#include <semaphore.h> // 初始化信號量 int sem_init(sem_t *sem, int pshared, unsigned int value); // 信號量 P 操作(減 1) int sem_wait(sem_t *sem); // 以非阻塞的方式來對信號量進行減 1 操作 int sem_trywait(sem_t *sem); // 信號量 V 操作(加 1) int sem_post(sem_t *sem); // 獲取信號量的值 int sem_getvalue(sem_t *sem, int *sval); // 銷毀信號量 int sem_destroy(sem_t *sem);
// 信號量用於同步實例 #include <stdio.h> #include <unistd.h> #include <pthread.h> #include <semaphore.h> sem_t sem_g,sem_p; //定義兩個信號量 char ch = 'a'; void *pthread_g(void *arg) //此線程改變字符ch的值 { while(1) { sem_wait(&sem_g); ch++; sleep(1); sem_post(&sem_p); } } void *pthread_p(void *arg) //此線程打印ch的值 { while(1) { sem_wait(&sem_p); printf("%c",ch); fflush(stdout); // 刷新標准輸出緩沖區 sem_post(&sem_g); } } int main(int argc, char *argv[]) { pthread_t tid1,tid2; sem_init(&sem_g, 0, 0); // 初始化信號量為0 sem_init(&sem_p, 0, 1); // 初始化信號量為1 // 創建兩個線程 pthread_create(&tid1, NULL, pthread_g, NULL); pthread_create(&tid2, NULL, pthread_p, NULL); // 回收線程 pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; }
結果會依次打印26個字母,代碼的執行順序是,打印線程------>自增線程
參考:https://blog.csdn.net/daaikuaichuan/article/details/82950711
https://www.cnblogs.com/yinbiao/p/11190336.html