包括線程概念簡介;線程創建函數pthread_create以及退出、回收等;線程同步互斥鎖pthread_mutex_t,讀寫鎖pthread_rwlock_t,條件變數pthread_cond_t以及信號量semaphore ...
1. 線程的概念
【操作系統】2.進程和線程 - imXuan - 博客園 (cnblogs.com)
- 線程:light weight process(LWP)輕量級的進程,在 Linux 中本質上仍然是一個進程
- 進程:有獨立的地址空間,獨立PCB,可以當作只有一個線程的進程。進程是電腦資源分配的最小單位
- 線程:有獨立的PCB,共用物理地址空間,是最小的執行單位。cpu時間片劃分以PCB為依據,是調度的基本單位
- LWP號:cpu劃分時間片的依據。指令 " ps -Lf pid " 查看
1.1 線程共用的資源
- 1) 文件描述符表
- 2) 每種信號的處理方式
- 3) 當前工作目錄
- 4) 用戶ID和組ID
- 記憶體地址空間 (.text/.data/.bss/heap/共用庫) -> 堆區全局共用變數是共用的(進程是讀時共用寫時複製,實際上就是非共用)
1.2 線程非共用資源
- 1) 線程id
- 2) 處理器現場和棧指針(內核棧)
- 3) 獨立的棧空間(用戶空間棧)
- 4) errno變數
- 5) 信號屏蔽字(共用信號,但是可以使用信號屏蔽字)
- 6) 調度優先順序
1.3 線程優缺點
- 優點:
- 提高程式併發性
- 開銷小
- 數據通信、共用數據方便
- 缺點:
- 庫函數,不穩定
- 調試、編寫困難、gdb不支持
- 對信號支持不好
- 優點相對突出,缺點均不是硬傷。Linux下由於實現方法導致進程、線程差別不是很大。
2. 線程常用操作
- 創建線程:pthread_create
- 線程獲取:pthread_self
- 線程退出:
- 線程內部:return void* (0);
- 線程內部:pthread_exit(void *(0));
- 線程外部:pthread_canel(返回值是 -1,需要一個取消點)
- 線程回收
- 手動回收:pthread_join
- 自動回收:pthread_detach
2.1 創建線程 pthread_create
功能:創建一個線程。
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg ); /* 參數: thread:傳出參數。線程標識符地址(一個無符號數)。 attr:線程屬性結構體地址,通常設置為 NULL。 start_routine:線程函數的入口地址。 arg:傳給線程函數的參數。 返回值: 成功:0 失敗:非 0 */
線程中處理出錯
#include <string.h> char *strerror(int errnum); fprintf(stderr, "xxx error: %s\n", strerror(錯誤號));
2.2 獲取線程ID pthread_self
功能:獲取線程號(與ps -Lf 查看的 id 不同)
#include <pthread.h> pthread_t pthread_self(void); /* 參數:無 返回值 調用線程的線程 ID 。 */
2.3 線程退出 pthread_exit
功能:退出調用線程。一個進程中的多個線程是共用該進程的數據段,因此,通常線程退出後所占用的資源並不會釋放。
- return:返回到調用者
- exit:退出進程
- pthread_exit:退出線程
#include <pthread.h> void pthread_exit(void *retval); /* 參數:retval:存儲線程退出狀態的指針。 返回值:無 */
2.4 線程回收 pthread_join
功能:等待線程結束(此函數會阻塞),並回收線程資源,類似進程的 wait() 函數。如果線程已經結束,那麼該函數會立即返回。
#include <pthread.h> int pthread_join(pthread_t thread, void **retval); /* 參數: thread:被回收的線程號。 retval:用來存儲線程退出狀態的指針的地址 (pthread_exit 退出返回值是 void*, 這裡是一個指針, 指向 void*指針, 所以是void**) 返回值: 成功:0 失敗:非 0 */
2.5 線程分離 pthread_detach
功能:使調用線程與當前進程分離,分離後不代表此線程不依賴與當前進程,線程分離的目的是將線程資源的回收工作交由系統自動來完成,也就是說當被分離的線程結束之後,系統會自動回收它的PCB資源。所以,此函數不會阻塞。
#include <pthread.h> int pthread_detach(pthread_t thread); /* 參數:thread:線程號。 返回值: 成功:0 失敗:非0 */
2.6 線程取消 pthread_cancel
功能:殺死(取消)線程
- 被 pthread_cancel() 殺死的線程,使用 pthread_join() 再進行回收,會得到返回值 -1
- 使用 pthread_cancel() 殺死線程必須有一個保存點才能生效,否則無法殺死線程。應該在被 cancel 函數調用的線程函數里自己添加一個取消點 pthread_testcancel(); 實際上就是進入系統內核,給他一個殺死線程的機會
#include <pthread.h> int pthread_cancel(pthread_t thread); /* 參數:thread : 目標線程ID。 返回值: 成功:0 失敗:出錯編號 */
3. 線程同步
3.1 互斥鎖 pthread_mutex_t
互斥鎖是一種簡單的加鎖的方法來控制對共用資源的訪問,互斥鎖只有兩種狀態,即加鎖( lock )和解鎖( unlock )
#include <pthread.h> // 創建互斥鎖 pthread_mutex_t mutex; // 靜態初始化 互斥鎖 mutex = PTHREAD_MUTEX_INITIALIZER; // 動態初始化 互斥鎖, attr:設置互斥量的屬性, NULL表示預設 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); // 銷毀指定的一個互斥鎖。互斥鎖在使用完畢後,必須要對互斥鎖進行銷毀,以釋放資源 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);
3.2 讀寫鎖 pthread_rwlock_t
當有一個線程已經持有互斥鎖時,互斥鎖將所有試圖進入臨界區的線程都阻塞住。但是當前持有互斥鎖的線程只是要讀訪問共用資源,而同時有其它幾個線程也想讀取這個共用資源,但是由於互斥鎖的排它性,所有其它線程都無法獲取鎖,也就無法讀訪問共用資源了,但是實際上多個線程同時讀訪問共用資源並不會導致問題。
在對數據的讀寫操作中,更多的是讀操作,寫操作較少,例如對資料庫數據的讀寫應用。為了滿足當前能夠允許多個讀出,但只允許一個寫入的需求,線程提供了讀寫鎖來實現。
讀寫鎖的特點:
- 如果有其它線程讀數據,則允許其它線程執行讀操作,但不允許寫操作
- 如果有其它線程寫數據,則其它線程都不允許讀、寫操作
讀寫鎖分為讀鎖和寫鎖,規則如下:
- 如果某線程申請了讀鎖,其它線程可以再申請讀鎖,但不能申請寫鎖
- 如果某線程申請了寫鎖,其它線程不能申請讀鎖,也不能申請寫鎖
舉例子:線程1 給讀寫鎖加了讀鎖,此時 線程2 請求讀鎖、線程3 請求寫鎖;則 線程2 的讀鎖會被阻塞,等 線程1 讀鎖釋放後,線程3 進行寫,之後 線程2 再讀
#include <pthread.h> // 初始化一個讀寫鎖(restrict 修飾指針變數, 被變數修飾的記憶體操作只能由本指針操作) 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);
3.3 條件變數 pthread_cond_t
與互斥鎖不同,條件變數是用來等待而不是用來上鎖的,條件變數本身不是鎖,條件變數用來自動阻塞一個線程,直到某特殊情況發生為止。通常條件變數和互斥鎖同時使用。生產者消費者模型中比較常用
條件變數的兩個動作:
- 條件不滿, 阻塞線程
- 當條件滿足, 通知阻塞的線程開始工作
條件變數的類型: pthread_cond_t
#include <pthread.h> // 初始化一個條件變數 int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); // 銷毀一個條件變數 int pthread_cond_destroy(pthread_cond_t *cond); // 等待條件滿足 int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); /* * 功能: * 1.阻塞等待一個條件變數 * 2.解鎖已經加鎖成功的互斥量 (1.2為原子操作) * .....等待..... * 3.當條件滿足,函數返回時,重新加鎖互斥量 */ // 等待條件滿足, 超時退出 int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec*restrict abstime); // 喚醒阻塞在條件變數上的線程 int pthread_cond_signal(pthread_cond_t *cond); // 喚醒所有阻塞在條件變數上的線程 int pthread_cond_broadcast(pthread_cond_t *cond);
timespec結構體(abs_time 表示絕對時間,從1970年1月1日 00:00:00計算)
struct timespec { time_t tv_sec; /* seconds */ // 秒 long tv_nsec; /* nanosecondes*/ // 納秒 } time_t cur = time(NULL); //獲取當前時間。 struct timespec t; //定義timespec 結構體變數t t.tv_sec = cur + 1; // 定時1秒 pthread_cond_timedwait(&cond, &t);
一個示例代碼
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <pthread.h> void err_thread(int ret, char* str) { if(ret!=0){ fprintf(stderr, "%s:%s\n", str, strerror(ret)); pthread_exit(NULL); } } // 創建公共區 struct msg{ int num; struct msg *next; }; struct msg *head = NULL; // 創建互斥量,初始化 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 創建條件變數,初始化 pthread_cond_t has_data = PTHREAD_COND_INITIALIZER; void* producer(void* arg) { int i = 0; while(1){ struct msg *p = malloc(sizeof(struct msg)); // 生產數據 p->num = ++i; p->next = NULL; printf("product: %d\n", p->num); // 將數據保存到公共區 pthread_mutex_lock(&mutex); p->next = head; head = p; pthread_mutex_unlock(&mutex); // 通知消費者 pthread_cond_signal(&has_data); sleep(rand() % 3); } return NULL; } void* consumer(void* arg) { while(1){ struct msg *mp; // 加鎖互斥量 pthread_mutex_lock(&mutex); // 判斷條件是否滿足 while (head==NULL) // 註意 while 迴圈才可以解決多消費者搶鎖的問題 { // 阻塞等待, 解鎖 pthread_cond_wait(&has_data, &mutex); } // 返回值, 重新加鎖 mp = head; head = mp->next; // 操作公共區結束, 立即解鎖 pthread_mutex_unlock(&mutex); printf("consumer:%d\n", mp->num); free(mp); sleep(rand() % 3); } return NULL; } int main() { int ret; pthread_t pid1, pid2, cid1, cid2, cid3; srand(time(NULL)); ret = pthread_create(&pid1, NULL, producer, NULL); if(ret!=0) err_thread(ret, "pthread_create producer:"); ret = pthread_create(&pid2, NULL, producer, NULL); if(ret!=0) err_thread(ret, "pthread_create producer:"); ret = pthread_create(&cid1, NULL, consumer, NULL); if(ret!=0) err_thread(ret, "pthread_create consumer:"); ret = pthread_create(&cid2, NULL, consumer, NULL); if(ret!=0) err_thread(ret, "pthread_create consumer:"); ret = pthread_create(&cid3, NULL, consumer, NULL); if(ret!=0) err_thread(ret, "pthread_create consumer:"); pthread_join(pid1, NULL); pthread_join(pid2, NULL); pthread_join(cid1, NULL); pthread_join(cid2, NULL); pthread_join(cid3, NULL); return 0; }
3.4 信號量 semaphore
信號量廣泛用於進程或線程間的同步和互斥,信號量本質上是一個非負的整數計數器,它被用來控制對公共資源的訪問。編程時可根據操作信號量值的結果判斷是否對公共資源具有訪問的許可權,當信號量值大於 0 時,則可以訪問,否則將阻塞
#include <semaphore.h> // 創建一個信號量並初始化它的值。一個無名信號量在被使用前必須先初始化。 // pshared 0:線程同步; 1: 進程同步 // value: 信號量的初值 // 成功返回0, 失敗返回-1, 設置errno int sem_init(sem_t *sem, int pshared, unsigned int value); // 刪除 sem 標識的信號量。 int sem_destroy(sem_t *sem); // 將信號量的值減 1。操作前,先檢查信號量(sem)的值是否為 0,若信號量為 0,此函數會阻塞,直到信號量大於 0 時才進行減 1 操作。 int sem_wait(sem_t *sem); // 以非阻塞的方式來對信號量進行減 1 操作。 // 若操作前,信號量的值等於 0,則對信號量的操作失敗,函數立即返回。 int sem_trywait(sem_t *sem); // 限時嘗試將信號量的值減 1 // abs_timeout:絕對時間 int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); // 將信號量的值加 1 併發出信號喚醒等待線程(sem_wait())。 int sem_post(sem_t *sem); // 獲取 sem 標識的信號量的值,保存在 sval 中。 int sem_getvalue(sem_t *sem, int *sval);
一個示例代碼
#include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <semaphore.h> #include <pthread.h> #define NUM 5 int queue[NUM]; sem_t blank_number, product_number; // 消費者 void* consumer(void *arg) { int i = 0; while(1) { sem_wait(&product_number); // 產品數量-- (0則阻塞) printf("Consume:%d\n", queue[i]); queue[i] = 0; sem_post(&blank_number); // 空格數量++ i = (i+1) % NUM; sleep(rand() % 6); } } // 生產者 void* producer(void *arg) { int i = 0; int number = 0; while(1) { sem_wait(&blank_number); // 空閑數量-- (0則阻塞) queue[i] = ++number; printf("Produce:%d\n", number); sem_post(&product_number); // 產品數量++ i = (i+1) % NUM; sleep(rand() % 2); } } int main() { pthread_t pid, cid; sem_init(&blank_number, 0, NUM); // 初始化空閑區域 sem_init(&product_number, 0, 0); // 初始化產品數量 pthread_create(&pid, NULL, producer, NULL); pthread_create(&cid, NULL, consumer, NULL); pthread_join(pid, NULL); pthread_join(cid, NULL); sem_destroy(&blank_number); sem_destroy(&product_number); return 0; }