線性(下)----執行緒安全
執行緒安全
因為程序中執行緒共享了程序中的虛擬地址空間,所以執行緒間的通訊更加方便,但資料有可能存在爭搶關係,缺乏資料的訪問控制,多個執行緒併發容易造成資料混亂,所以資料安全訪問變得很重要。 造成資料混亂的的兩個經典模型
同步與互斥概念
同步:執行緒/程序之間對臨界資源的順序訪問關係(對臨界資源訪問的時序性) 互斥:執行緒/程序之間對臨界資源的同一時間的唯一訪問性關係
生產者與消費者模型
一個場所,兩個角色,三種關係 生產者與生產者的關係:互斥(來保證資料的安全操作) 生產者與消費者的關係:同步和互斥 消費者與消費者的關係:互斥
如何來解決執行緒中資料的安全訪問?------->實現執行緒間互斥
執行緒間的互斥實現:互斥鎖(互斥量) 執行緒間的同步實現:條件變數 POSIX訊號量:既可以實現同步可以實現互斥,既可以用於程序間的同步互斥,也可以實現執行緒間的同步互斥。
在互斥鎖中死鎖的必要條件?—如何避免 條件變數----等待和通知 為什麼條件變數和互斥鎖一起使用? 對於實現同步關鍵在於等待和通知,因為等待需要被喚醒,被喚醒的前提條件就是條件已經滿足,並且這個條件本身就是一個臨界資源。
互斥鎖(或互斥量)-----實現執行緒間的互斥
互斥鎖原理: 互斥鎖以排他的方式防止共享資料被併發訪問,是一個二元變數, 本質就是一個計數器,計數器只有0/1,在處理臨界資源時要先申請互斥鎖。互斥鎖處於開鎖狀態,申請到互斥鎖後立即佔有該鎖(加鎖),防止其他執行緒訪問資源。只有當前鎖定該互斥鎖的執行緒才可以釋放該互斥鎖。
1.定義一個互斥鎖//線上程建立之前完成 定義一個互斥量(變數) pthread_mutex_t name 2.初始化互斥鎖 互斥鎖的初始化有兩種方式: 1.定義時賦值初始化,不需要手動釋放 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 2.函式介面初始化,需要手動釋放 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); //引數一 : 互斥鎖的變數 //引數二 : 互斥鎖的屬性,一般設定為NULL 返回0成功,返回非0就是錯誤 3.對臨界操作程序加鎖或解鎖 加鎖: int pthread_mutex_lock(pthread_mutex_t* mutex);//阻塞式申請,如果鎖被鎖住則等待鎖被開啟,即若該鎖是鎖頂狀態,預設阻塞當前程序。 int pthread_mutex_trylock(pthread_mutex_t* mutex);//非阻塞加鎖,獲取不到鎖立即報錯返回 int pthread_mutex_timedlock(pthread_mutex_t* restrict mutex, const struct timespec *restrict abs_timeout); //限時阻塞加鎖,如果獲取不到鎖則指定等待時間,這段時間完了還沒獲取到,則報錯返回 解鎖(釋放):int pthread_mutex_unlock(pthread_mutex_t *mutex);//在任意一個有任何可能性退出的地方都要解鎖 4.銷燬互斥鎖 pthread_mutex_destroy(pthread_mutex_t *restrict mutex);
死鎖情況:一直獲取不到鎖資源而造成的鎖死情況 死鎖產生的必要條件:必須具備以下條件才能滿足 全部具備以下條件: 1.互斥條件----一個獲取另外一個就不能獲取 2.不可剝奪條件----一個執行緒獲取鎖只能由這個執行緒自己釋放 3.請求與保持條件----獲取第一個鎖之後又去獲取第二個鎖 4.環路等待條件----a拿了鎖1去申請鎖2,而b拿了鎖2去申請鎖1,形成環路死鎖 如何預防產生死鎖:破壞死鎖產生的必要條件 避免產生死鎖:銀行家演算法(在這個演算法中定義了兩個狀態,安全狀態,非安全狀態,如果某一步操作操作完畢後處於安全狀態,那麼可以執行,如果處於不安全狀態那麼就不能執行)
線性間互斥例項:
/*
這是一個買票的例子
每一個黃牛都是一個執行緒,在這個例子中有一個總票數ticket
* 每一個黃牛買到一張票這個ticket都會-1,直到票數為0
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
int ticket = 100;
//互斥鎖的初始化有兩種方式:
// 1. 定義時直接賦值初始化,最後不需要手動釋放
// 2. 函式介面初始化,最後需要手動釋放
// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex; //定義互斥鎖
void *y_cow(void *arg)
{
int id = (int)arg;
while(1) {
//2. 加鎖操作
// int pthread_mutex_lock(pthread_mutex_t *mutex);
// 阻塞加鎖,如果獲取不到鎖則阻塞等待鎖被解開
// int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 非阻塞加鎖,如果獲取不到鎖則立即報錯返回EBUSY
// int pthread_mutex_timedlock (pthread_mutex_t *mutex,
// struct timespec *t);
// 限時阻塞加鎖,如果獲取不到鎖則等待指定時間,在這段
// 時間內如果一直獲取不到,則報錯返回,否則加鎖
pthread_mutex_lock(&mutex);
if (ticket > 0) {
usleep(100); //如果沒有進行加鎖操作,當票等於1時在睡眠的這個時間,很多執行緒都會進入,就會導致買到附屬的票
printf("y_cow:%d get a ticket:%d!!\n", id, ticket);
ticket--;
}else {
printf("have no ticket!!exit!!\n");
//**加鎖後,在任意有可能退出的地方都要進行解鎖,
//**否則會導致其他執行緒阻塞卡死
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
//int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 解鎖
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid[4];
int i = 0, ret;
//1. int pthread_mutex_init(pthread_mutex_t *mutex,
// const pthread_mutexattr_t *attr);
// 互斥鎖的初始化
// mutex: 互斥鎖變數
// attr:互斥鎖的屬性,NULL;
// 返回值:0-成功 errno-錯誤
pthread_mutex_init(&mutex, NULL);
for (i = 0; i < 4; i++) {
ret = pthread_create(&tid[i], NULL, y_cow, (void*)i);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
}
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
pthread_join(tid[2], NULL);
pthread_join(tid[3], NULL);
//4. 銷燬互斥鎖
pthread_mutex_destroy(&mutex);
return 0;
}
條件變數—實現執行緒間的同步
條件變數的原理: 互斥鎖能夠解決對資源的互斥訪問,但有些情況互斥並不能解決
同步說的是對公共資源的時序訪問,若有資源,則一個執行緒就會來訪問,如果沒有資源則執行緒就會等待,條件變數發生改變時就會進行通知,執行緒就會做相應工作。所以條件變數用於等待某個條件被觸發。 在這裡以生產消費者模型來詳細說明一下同步:
條件變數不能單獨使用,需要和互斥鎖配合使用,因為執行緒等待被喚醒,被喚醒的前提是“條件改變了”,例如沒有產品時 ,消費者等待,有產品時,消費者被喚醒,有無產品就是“這個條件”。 執行緒同步實現程式碼:
/* 這是一個實現生產者與消費者同步的程式碼,生產者消費者分別代表一個執行緒
* 有一個籃子,這個籃子是判斷條件,
* 籃子裡有面
* 代表消費者可以獲取面,通知生產者面已經取走了
* 代表生產者需要等待
* 籃子裡沒有面
* 代表消費者等待
* 代表生產者放面,通知消費者面已經放了
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
//1. 定義條件變數
// 條件變數的初始化有兩種方式
// 1. 定義賦值初始化,不需釋放
// 2. 函式介面初始化, 需要釋放
// pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond; //定義條件變數
pthread_mutex_t mutex; //定義互斥鎖
int basket = 0;
//賣面的
void *sale_noddle(void *arg)
{
while(1) {
pthread_mutex_lock(&mutex); //basket就是是一個判斷條件,執行緒都能訪問
//比如有多個生產者時(多個執行緒),對於這一個公共資料,那麼就會有爭搶行為,需要互斥鎖
if (basket == 0) { //加鎖實現了對這個全域性變數(公共資源的保護)
printf("sale noddle!!!\n");
basket == 1; //生產了面,然後開始通知對方,喚醒消費者,使其不再等待
//int pthread_cond_broadcast(pthread_cond_t *cond);
// 喚醒所有等待在條件變數上的執行緒
//int pthread_cond_signal(pthread_cond_t *cond);
// 喚醒第一個等待在條件變數上的執行緒
pthread_cond_signal(&cond);
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *buy_noddle(void *arg)
{
while(1) {
pthread_mutex_lock(&mutex); //加鎖保護
if (basket == 0) {
//沒有面就要等待
//int pthread_cond_wait(pthread_cond_t *cond,
// pthread_mutex_t *mutex);第二個引數就是互斥鎖
// pthread_cond_wait的功能就是用來阻塞等待某個條件變數。它做的事情就是先解鎖然後進入等待
// pthread_cond_wait函式先對互斥鎖做了一個判斷是否加鎖,如果加鎖了就解鎖
// 然後陷入等待*******整個過程是原子操作,不可被打斷。
//
// 要防止的情況就是:假如沒有面,而消費者又速度比較
// 快,先拿到鎖了,那麼生產者將拿不到鎖,沒法生產將會
// 造成雙方卡死
// 所以如果消費者如果先獲取到鎖,那麼在陷入等待之前需
// 要解鎖
pthread_cond_wait(&cond, &mutex);
}
printf("buy noddles!!!\n");
basket = 0;
pthread_mutex_unlock(&mutex); //解鎖
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
int ret;
//1. 條件變數的初始化
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
ret = pthread_create(&tid1, NULL, sale_noddle, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
ret = pthread_create(&tid2, NULL, buy_noddle, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//4. 條件變數的銷燬
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
POSIX 標準訊號量----既可以實現同步也可以實現互斥
即可用於程序也可以用於執行緒
POSIX訊號量實現執行緒間的同步和互斥
**訊號量本質:**具有一個等待佇列的計數器
執行緒同步實現: 消費者:沒有資源則等待 生產者:生產出來資源則通知等待佇列中的等待者
/* 這是驗證使用訊號量還實現執行緒間同步與互斥的程式碼
訊號量的操作步驟:
* 1. 訊號量的初始化
* 2. 訊號量的操作(等待/通知)
* 3. 訊號量的釋放
* 1. 同步:等待與通知
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h>
sem_t sem;
//執行緒間同步與互斥
void *thr_producer(void *arg)
{
while(1) {
//生產者
sleep(1);
printf("make a hot beef noddle!!\n");
//生產出資源後要通知等待在訊號量上的執行緒/程序
//int sem_post(sem_t *sem);
//訊號量修改的是自己內部的資源計數,這個內部的資源計數就是
//條件,而條件變數修改的是外部的條件,需要我們使用者來修改
sem_post(&sem);
}
return NULL;
}
void *thr_consumer(void *arg)
{
while(1) {
//消費者
//2. 沒有資源則等待
//阻塞等待,沒有資源則一直等待有資源,否則獲取資源
//int sem_wait(sem_t *sem);
//非阻塞等待,沒有資源則報錯返回,否則獲取資源
//int sem_trywait(sem_t *sem);
//限時等待,沒有資源則等待指定時長,這段時間內有資源則獲取
//一直沒有資源則超時後報錯返回
//int sem_timedwait(sem_t *sem,struct timespec *timeout);
sem_wait(&sem);
printf("very good!!!\n");
}
return NULL;
}
int ticket = 100;
void *buy_ticket(void *arg)
{
while(1){
//大家都是黃牛!!
//因為計數器最大是1,也就代表只有一個執行緒能夠獲取到訊號量
//這樣也就保證了同一時間只有一個執行緒能操作
sem_wait(&sem);
if (ticket > 0) {
usleep(1000);
ticket--;
printf("cow %lu,buy a ticket:%d\n", ticket);
}
//操作完畢之後,對計數器進行+1,這時候訊號量資源計數就又可
//以獲取了,然後又進入新一輪的資源爭搶,因為資源計數只有一
//個,因此也只有一個執行緒能夠搶到
sem_post(&sem);
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
int ret;
//1. 初始化訊號量
//int sem_init(sem_t *sem, int pshared, unsigned int value);
// sem:訊號量變數
// pshared:
// 0-用於執行緒間
// 非0-用於程序間
// value:訊號量的初始計數
ret = sem_init(&sem, 0, 1);
if (ret < 0) {
printf("init sem error!!\n");
return -1;
}
/*
//建立生產者執行緒
ret = pthread_create(&tid1, NULL, thr_producer, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
//建立消費者執行緒
ret = pthread_create(&tid2, NULL, thr_consumer, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
*/
//黃牛買票執行緒
pthread_t tid;
int i = 0;
for (i = 0; i < 4; i++) {
ret = pthread_create(&tid, NULL, buy_ticket, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
}
pthread_join(tid, NULL);
//3. 銷燬訊號量
//int sem_destroy(sem_t *sem);
sem_destroy(&sem);
return 0;
}
資源爭搶的另外一種模型------讀寫者模型(理解即可),實現讀寫模型的安全資料訪問是用—讀寫鎖
讀寫者模型: 大量讀,少量寫。 寫的時候他人不能讀, 讀的時候不能寫, 寫的時候他人不能寫, 讀的時候他人可以讀
互相關係 : 讀寫之間互斥 寫於寫之間互斥 讀和讀沒有關係 讀寫鎖的實現------讀寫鎖瞭解即可