執行緒控制(執行緒同步 & 執行緒安全)
一、執行緒同步
當多個控制執行緒共享相同的記憶體時,需要確保每個執行緒看到一致的資料。如果當某個執行緒可以去修改變數,而其他執行緒也可以去讀取或者修改這個變數的時候,就需要對這些執行緒進行同步控制,以確保它們在訪問變數的儲存內容時不會訪問到無效的數值。
同步:多程序或者多執行緒訪問臨界資源時,必須進行同步控制。多程序或者多執行緒的執行並不是完全絕對的並行執行,有可能主執行緒需要等待函式執行緒的某些條件的發生。
多執行緒之間有幾個特殊的臨界資源:全域性資料、堆區資料、檔案描述符 多執行緒之間公用
執行緒間同步控制的方式:
(1)訊號量
標頭檔案: #include <semaphore.h>
獲取:int sem_init(sem_t *sem, int shared, int value);
sem:是一個 sem_t 型別指標,指向訊號量物件
shared:表示是否能在多個程序之間共享,Linux 下不支援,為 0
value:訊號量的初始值
該函式成功時返回為 0,失敗時返回為 -1
P 操作:int sem_wait(sem_t *sem); //阻塞執行
V 操作:int sem_post(sem_t *sem);
刪除:int sem_destroy(sem_t *sem);
練習:主執行緒迴圈獲取使用者輸入,函式執行緒統計使用者輸入的字元個數(統計一次需要 1 秒)
程式碼如下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <assert.h> #include <pthread.h> char buffer[128] = {0}; pthread_mutex_t mutex; void *pthread_count(void *arg) { while (1) { pthread_mutex_lock(&mutex); if (strncmp(buffer, "OK", 2) == 0) { break; } printf("count = %d\n", strlen(buffer)); pthread_mutex_unlock(&mutex); sleep(1); } pthread_exit(NULL); } void main() { pthread_t id; pthread_mutex_init(&mutex, NULL); int res = pthread_create(&id, NULL, pthread_count, NULL); assert(res == 0); printf("Please input: \n"); while (1) { pthread_mutex_lock(&mutex); fgets(buffer, 127, stdin); buffer[strlen(buffer)-1] = 0; pthread_mutex_unlock(&mutex); if (strncmp(buffer, "end", 3) == 0) { exit(0); } sleep(1); } pthread_exit(NULL); }
執行結果如下:
(2)互斥鎖
假設執行緒 A 讀取變數然後給這個變數賦予一個新的值,但寫操作需要兩個儲存器週期。當執行緒 B 在這兩個儲存器寫週期中間讀取這個相同的變數時,它就會得到不一致的值。為了解決這個問題,執行緒不得不使用鎖,在同一時間只允許一個執行緒訪問該變數。如果執行緒 B 希望讀取變數,它首先要獲取鎖;同樣的,當執行緒 A 更新變數時,也需要獲取這把同樣的鎖。因而執行緒 B 線上程 A 釋放以前不能讀取變數。
可以通過使用 pthread 的互斥介面保護資料,確保同一時間只有一個執行緒訪問資料。互斥量(mutex)從本質上說是一把鎖,在訪問共享資源之前對互斥量進行加鎖,在訪問完成之後釋放互斥量的鎖。對互斥量進行加鎖以後,任何其他試圖再次對互斥量進行加鎖的執行緒將會被阻塞值到當前執行緒釋放該互斥鎖。如果釋放互斥鎖時有多個執行緒阻塞,所有在該互斥鎖上的阻塞執行緒都會變成可執行狀態,第一個變為執行狀態的執行緒可以對互斥量加鎖,其他執行緒將會看到
概念:完全控制臨界資源,如果一個執行緒完成加鎖操作,則其他執行緒無論如何都無法再完成加鎖,也就無法對臨界資源進行訪問
初始化:int pthread_mutex_init(pthread_mutex_t *mutex, pyhread_mutex_attr_t *attr);
加鎖: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);
(3)條件變數
二、執行緒安全 ----> 可重入函式
有些庫函式會使用執行緒間共享的資料,如果沒有同步控制,執行緒操作就是不安全的,所以,我麼們使用這樣一些函式時,就必須使用其安全的版本 -----> 可重入函式
三、執行緒中 fork 的使用,鎖的變化
線上程中呼叫 fork 函式,子程序只會啟用呼叫 fork 函式的那條執行緒,其他執行緒不會啟用。
子程序會繼承其父程序的鎖以及其鎖的狀態,但是父程序和子程序使用的不是同意把鎖,父程序解鎖並不會影響到子程序的鎖,所以子程序有可能死鎖!!!
pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
pthread_atfork() 函式在 fork() 之前呼叫,當呼叫 fork() 時,內部建立子程序前在父程序中會呼叫 prepare,內部建立子程序後,父程序會呼叫 parent,子程序會呼叫 child。
指定在 fork 呼叫之後,建立子程序之前,呼叫 prepare 函式,獲取所有的鎖,然後建立子程序,子程序建立以後,父程序環境中呼叫 parent 解所有的鎖,子程序環境中呼叫 child 解所有的鎖,然後 fork 函式再返回。這樣保證了在 fork 之後,子程序拿到的所都是解鎖狀態,從而避免死鎖。