文章目錄
線程
線程概念
進程: 傳統操作系統上的進程是pcb,操作系統通過pcb控制程序運行
線程: 但是Linux下線程使用pcb實現,Linux下pcb是線程,也叫輕量級進程(相較於傳統進程更加輕量化— 同一個進程中的線程公用同一個虛擬地址空間);進程是線程組
進程是資源分配的基本單位—資源是分配給整個線程組的
線程是cpu調度的基本單位—cpu調度通過pcb調度程序的運行
線程之間的獨有與共享:
獨有 | 共享 |
---|---|
棧 | 虛擬地址空間(代碼段、數據段):mm_struct |
寄存器 | 文件描述符表:files_struct |
信號屏蔽字 | 信號處理方式:sighand_struct |
errno | 當前工作路徑 |
線程標識符 | 用戶ID,組ID |
線程和進程優缺點對比及使用場景:
優點:
- 同一個進程中的線程共享同一個虛擬地址空間
- 線程間通信更加方便
- 線程的創建與銷燬相比於進程成本更低
- 線程的調度成本要更低一點
- 線程的執行力度要更加細緻
缺點:
- 線程間缺乏訪問控制,編碼難度更高
- 線程健壯性更低
- 性能損失
注意:除非對主程序安全性、穩定性要求特別高的程序使用多進程處理多任務;其它大多都是多線程
線程控制:
yum -y install man-pages //我使用的centos7,無法使用man手冊就運行前面的命令,安裝支持posix函數的man手冊
操作系統並沒有提供創建線程的系統調用接口,因此大佬們封裝了一個線程的接口庫實現線程控制;意味着用戶創建線程都使用的庫函數(所以有時候我們說創建的線程是一個用戶態線程,但是在內核中對應有一個輕量級進程實現線程程序的調度)
線程創建:線程間不存在父子關係
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.
pthread_create 每個線程都有自己獨有的一塊空間,並且這塊空間在進程的虛擬地址空間中
pcb->pid pcb->tgid tid
LWP PID
輕量級進程id 進程ID 線程空間首地址
進程ID-線程組ID–tgid等於主線程的pid
- 線程是庫函數創建的,如果用戶想要操作線程就必須擁有線程的操作句柄,而這個tid是線程地址空間的首地址,線程地址空間中保存了線程的數據,用戶可以通過這個tid實現線程控制
- 每個線程都有自己的獨立空間,並且這些空間也是在進程的虛擬地址空間內
/*線程創建:
* int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*stact_routine) (void *), void *arg);
* thread:用於獲取被創建的新線程id
* attr:用於設置線程屬性,通常置NULL
* start_routine:線程入口函數
* arg:傳遞給線程的參數,作爲start_routine的實參傳入
* 返回值:0 失敗:!=0-----errno*/
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void *thr_start(void *arg)
{
while(1) {
printf("i am normal thread---ptr:%s\n", (char*)arg);
sleep(1);
}
return NULL;
}
int main (int argc, char *argv[])
{
pthread_t tid;
int ret;
char *ptr = "leihoua~~~";
ret = pthread_create(&tid, NULL, thr_start, (void*)ptr);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
while(1) {
printf("i am main thread-----new thread id:%p\n", tid);
sleep(1);
}
return 0;
}
線程終止:進程退出
在任意線程中不能使用exit退出,因爲exit退出的也是整個進程
在普通線程中使用return退出(在 main函數中不能使用return退出,因爲退出的是進程)
pthread_exit 主動退出
int pthread_cancel(pthread_t thread) 取消線程,退出的線程被動取消 thread:要取消的線程id
線程退出也是會稱爲(殭屍進程)的
線程等待:
等待指定的線程退出–獲取退出線程的返回值,允許操作系統回收退出線程的資源
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Compile and link with -pthread.
一個資源創建成功後,默認有一個屬性是joinable屬性,處於joinable屬性的線程退出後,不會自動回收資源,需要被其他線程等待
只有處於joinable屬性的線程才能被等待,也必須被等待,因爲這種線程退出後資源不會自動回收
線程分離:
#include <pthread.h>
int pthread_detach(pthread_t thread);
Compile and link with -pthread.
分離一個線程,將線程的屬性從joinable設置爲detach屬性;
處於detach屬性的線程,退出後,自動回收資源;
處於detach屬性的線程,退出後,不需要被等待;
分離一個線程的前提就是用戶對線程的退出返回值,根本不關心;pthread_detach
線程分離可以是任意線程在任意時刻進行分離(線程一旦被創建直接分離;線程入口函數中分離自己)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *thr_start(void *arg)
{
//pthread_self(void)
// 獲取調用線程ID
pthread_detach(pthread_self());
sleep(5);
pthread_exit("nihaoa~~");
while(1) {
printf("i am ordinary thread\n");
sleep(1);
//void pthread_exit(void *retval);
//退出調用線程,誰調用,誰退出
//retval: 線程退出返回值
//pthread_exit(NULL);
}
return NULL;
}
int main (int argc, char *argv[])
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if (ret != 0) {
printf("thread create error\n");
return -1;
}
//int pthread_cancel(pthread_t thread);
// 取消線程,退出的線程被動取消
// thread: 要取消的線程id
//pthread_cancel(tid);
//int pthread_join(pthread_t thread, void **retval);
// 阻塞等待指定線程退出
// thread: 線程id
// retval: 用於獲取線程退出返回值
//int pthread_detach(pthread_t thread);
// 分離指定的線程
pthread_detach(tid);
char *ptr = NULL;
pthread_join(tid, (void**)&ptr);
printf("ordinary thread exit val:%s\n", ptr);
while(1) {
printf("i am main thread\n");
sleep(1);
}
return 0;
}
線程安全:
多個線程是否對同一個臨界資源進行了不受保護的非原子操作
線程安全就是多個線程對臨界資源進行操作,而不會造成數據二義性,反之是不安全
如何實現線程安全(同步與互斥)
同步: 臨界資源訪問的時序可控—保證訪問的合理性
互斥: 臨界資源同一時間的唯一訪問—保證訪問的安全性
如何實現互斥
互斥鎖mutex
模擬一個黃牛買票程序
原理: 原子操作的一個只具有0/1的計數器
互斥鎖使用步驟:
pthread_mutex_t 定義互斥鎖變量
pthread_mutex_init 初始化互斥鎖
pthread_mutex_lock 加鎖
pthread_mutex_unlock 解鎖
pthread_mutex_destroy 銷燬互斥鎖
死鎖:
因爲多方搶奪鎖資源,但是因爲搶奪推進順序不當引起死鎖導致相互僵持,產生程序無法推進卡死
產生必要條件:
- 互斥條件—指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。如果此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。(我加了鎖,別人就不能再加)
- 不可剝奪條件—指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。(我加的鎖,別人不能解)
- 請求與保持條件—指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不放。(拿到第一個鎖後,去請求第二個鎖,但是第二個鎖獲取不到,第一個鎖也不釋放)
- 環路等待條件—指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1佔用的資源;P1正在等待P2佔用的資源,……,Pn正在等待已被P0佔用的資源。
如何預防出現一個死鎖: 破壞必要條件
如何避免/處理產生一個死鎖: 銀行家算法,死鎖檢測算法
同步的實現:條件變量
同步的實現:等待與喚醒功能
條件不滿足則陷入等待,其它線程促使條件滿足後,喚醒等待
我想吃麪—>沒有面則需要等待做出來
廚師做面—>做出面後喚醒我的等待
模擬實現吃麪做面
pthread_cond_t 定義條件變量
pthread_cond_init 初始化條件變量
pthread_cond_wait 等待
pthread_cond_signal 喚醒
pthread_cond_destroy 銷燬條件變量
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);//限時等待,超時後報錯返回
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);//阻塞死等
int pthread_cond_broadcast(pthread_cond_t *cond);//喚醒所有等待
int pthread_cond_signal(pthread_cond_t *cond);//至少喚醒一個等待
int pthread_cond_destroy(pthread_cond_t *cond);
cond:條件變量
attr:條件變量屬性,通常置NULL
mutex:互斥鎖
abstime:超時等待時間
條件變量是搭配互斥鎖一起使用的:
- 因爲條件變量實現同步只提供了等待與喚醒功能,並沒有提供條件判斷的功能,因此條件判斷需要用戶實現,但是條件的操作是一個臨界資源的操作,因此需要受保護,需要在條件判斷之前加鎖;
- 如果加鎖成功後,因爲條件不滿足而陷入休眠,就會導致卡死(另一方因爲無法獲取鎖,而導致無法促使條件滿足),因此需要在休眠之前解鎖,並且解鎖與休眠必須是原子操作
- 被喚醒之後,即將對臨界資源進行了操作(吃麪的人被喚醒就即將開始吃麪)。但是吃麪之前需要進行保護加鎖
- 所以pthread_cond_wait集合了三步原子操作:解鎖—>休眠等待—>被喚醒後加鎖
條件判斷需要循環判斷 —防止多次消費/多次生產導致數據二義邏輯混亂
不同的角色需要等待在不同的條件變量上 —防止角色喚醒錯誤導致阻塞