c/c++:線程同步(互斥鎖、死鎖、讀寫鎖、條件變量、生產者和消費者模型、信號量)

目錄

1. 概念

2. 互斥鎖

3. 死鎖

4. 讀寫鎖

5. 條件變量

5.1 生產者和消費者模型

6. 信號量


 

1. 概念

  • 線程同步:

 > 當有一個線程在對內存進行操作時,其他線程都不可以對這個內存地址進行操作,直到該線程完成操作。
  > - 在多個線程操作一塊共享數據的時候
  >   - 按照先後順序依次訪問
  >   - 有原來的 並行 -> 串行

  • 臨界資源:一次只允許一個線程使用的資源。
  • 原子操作:

  > 原子操作,就是說像原子一樣不可再細分不可被中途打斷。

  > 一個操作是原子操作,意思就是說這個操作是以原子的方式被執行,要一口氣執行完,執行過程不能夠被OS的其他行爲打斷,是一個整體的過程,在其執行過程中,OS的其它行爲是插不進來的。

 

2. 互斥鎖

  • 互斥鎖類型:
// pthread_mutex_t 互斥鎖的類型
pthread_mutex_t mutex;
  • 互斥鎖特點:讓多個線程, 串行的處理臨界區資源(一個代碼塊)
  • 互斥鎖相關函數:
  #include <pthread.h>

  // 初始化互斥鎖
  int pthread_mutex_init(pthread_mutex_t *restrict mutex,
             const pthread_mutexattr_t *restrict attr);
  	參數: 
  		- mutex: 互斥鎖的地址
  		- attr: 互相鎖的屬性, 使用默認屬性, 賦值爲NULL就可以

  // 釋放互斥鎖資源
  int pthread_mutex_destroy(pthread_mutex_t *mutex);

  // 將參數指定的互斥鎖上鎖
  // 比如: 3個線程, 第一個線程搶到了鎖, 對互斥鎖加鎖 -> 加鎖成功, 進入了臨界區
  //  第二,三個個線程也對這把鎖加鎖, 因爲已經被線程1鎖定了, 線程2,3阻塞在了這把鎖上 -> 不能進入臨界區,
  // 	當這把鎖被打開, 線程2,3解除阻塞, 線程2,3開始搶鎖, 誰搶到誰加鎖進入臨界區, 另一個繼續阻塞在鎖上
  int pthread_mutex_lock(pthread_mutex_t *mutex);

  // 嘗試加鎖, 如果這把鎖已經被鎖定了, 加鎖失敗, 函數直接返回, 不會阻塞在鎖上
  int pthread_mutex_trylock(pthread_mutex_t *mutex);

  // 解鎖函數
  int pthread_mutex_unlock(pthread_mutex_t *mutex);

其中:

  restrict: 修飾符, 被修飾過的變量特點: 不能被其他指針引用
  	- mutex變量對應一塊內存
  	- 舉例: pthread_mutex_t* ptr; ptr = &mutex; // error
  	-  即便做了賦值, 使用ptr指針操作mutex對應的內存也是不允許的

 

3. 死鎖

兩個或兩個以上的進程在執行過程中,因爭奪共享資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖 。 

死鎖幾種場景:

  • 忘記釋放鎖,自己將自己鎖住
  • 單線程重複申請鎖
  • 多線程多鎖申請, 搶佔鎖資源(線程A有一個鎖1,線程B有一個鎖2。線程A試圖調用lock來獲取鎖2就得掛起等待線程B釋放,線程B也調用lock試圖獲得鎖1。都在等對方釋放,然後獲得對方的鎖。)

 

4. 讀寫鎖

  • 讀寫鎖類型? 是幾把鎖?

      1. 讀寫鎖是一把鎖
      2. 鎖定讀操作, 鎖定寫操作
      3. 類型: pthread_rwlock_t

  • 讀寫鎖的特點

/*
      1. 讀操作可以並進行, 多個線程
      2. 寫的時候獨佔資源的
      3. 寫的優先級高於讀的優先級
*/
場景:

  •       // 1. 線程A加讀鎖成功, 又來了三個線程, 做讀操作, 可以加鎖成功----讀操作是共享的, 三個新來的線程可以加讀鎖成功
  •       // 2. 線程A加寫鎖成功, 又來了三個線程, 做讀操作, 三個線程阻塞-------加讀鎖失敗, 會阻塞在讀鎖上, 寫完了
  •       // 3. 線程A加讀鎖成功, 又來了B線程加寫鎖阻塞, 又來了C線程加讀鎖阻塞------寫的獨佔的, 寫的優先級高
  • 什麼時候使用讀寫鎖?

互斥鎖: 數據所有的讀寫都是串行的
讀寫鎖:
       - 讀: 並行
       - 寫: 串行
  讀的頻率 > 寫的頻率

  • 操作函數:
  #include <pthread.h>
  // 初始化讀寫鎖
  int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
             const pthread_rwlockattr_t *restrict attr);
  	參數:
  		- rwlock: 讀寫鎖地址
  		- attr: 讀寫鎖屬性, 使用默認屬性, 設置爲: NULL

  // 釋放讀寫鎖資源
  int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

  // 加讀鎖
  // rwlock被加了寫鎖, 這時候阻塞
  // rwlock被加了讀鎖, 不阻塞, 可以加鎖成功 -> 讀共享
  int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

  // 嘗試加讀鎖
  int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

  // 加寫鎖
  // 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);

 

練習例子:  8個線程操作同一個全局變量,其中3個線程不定時寫同一全局資源,其中5個線程不定時讀同一全局資源

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

int number = 1;
pthread_rwlock_t rwlock;

void* writeNum(void* arg)
{
	while(1)
	{
		pthread_rwlock_wrlock(&rwlock);
		number++;
		usleep(100);
		printf("+++ write, tid: %ld, number: %d\n", pthread_self(), number);
		pthread_rwlock_unlock(&rwlock);
		usleep(100);
	}
	return NULL;
}

void* readNum(void* arg)
{
	while(1)
	{
		pthread_rwlock_rdlock(&rwlock);
		printf("=== read, tid: %ld, number: %d\n", pthread_self(), number);
		pthread_rwlock_unlock(&rwlock);
		usleep(100);
	}
	return NULL;
}

int main(int argc, char *argv[])
{
	pthread_t wtid[3], rtid[5];
	//初始化鎖
	pthread_rwlock_init(&rwlock, NULL);
	//創建寫進程
	for (int i=0; i<3; ++i)
	{
		pthread_create(&wtid[i],NULL, writeNum, NULL);
	}
	//創建讀進程
	for (int i=0; i<5; ++i)
	{
		pthread_create(&rtid[i], NULL, readNum, NULL);
	}

	//回收進程
	for (int i=0; i<3; ++i)
	{
		pthread_join(wtid[i], NULL);
	}
	for (int i=0; i<5; ++i)
	{
		pthread_join(rtid[i], NULL);
	}
	//銷燬鎖
	pthread_rwlock_destroy(&rwlock);
	return 0;
}

 

5. 條件變量

  • 條件變量不是鎖
  • 條件變量兩個動作:

條件變量能引起某個線程的阻塞具體來說就是:

  1.    - 某個條件滿足之後, 阻塞線程
  2.    - 某個條件滿足, 線程解除阻塞

如果使用了條件變量進行線程同步, 多個線程操作了共享數據, 不能解決數據混亂問題,解決該問題, 需要配合使用互斥鎖

  • 條件變量類型
pthread_cond_t
  • 條件變量操作函數
#include <pthread.h>
  // 初始化條件變量
  int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
  	參數: 
  		- cond: 條件變量的地址
  		- attr: 使用默認屬性, 這個值設置爲NULL

  // 釋放資源
  int pthread_cond_destroy(pthread_cond_t *cond);

  // 線程調用該函數之後, 阻塞
  int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
  	參數:
  		- cond: 條件變量
  		- mutex: 互斥鎖
  		
  struct timespec {
  	time_t tv_sec;      /* Seconds */
  	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
   };
  // 在指定的時間之後解除阻塞
  int pthread_cond_timedwait(pthread_cond_t *restrict cond,
             pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
  	參數:
  		- cond: 條件變量
  		- mutex: 互斥鎖
  		- abstime: 阻塞的時間
  			- 當前時間 + 要阻塞的時長
  				struct timeval val;
  			可以使用函數:gettimeofday(&val, NULL);

  // 喚醒一個或多個阻塞在 pthread_cond_wait / pthread_cond_timedwait 函數上的線程
  int pthread_cond_signal(pthread_cond_t *cond);

  // 喚醒所有的阻塞在 pthread_cond_wait / pthread_cond_timedwait 函數上的線程
  int pthread_cond_broadcast(pthread_cond_t *cond);

 

5.1 生產者和消費者模型

角色分析:
      - 生產者
      - 消費者
      - 容器

栗子:使用條件量實現 生產線和消費者模型: 生產者往鏈表中添加節點, 消費者刪除鏈表節點

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

pthread_cond_t cond;         //條件變量
pthread_mutex_t mutex;       //互斥鎖

//連表節點
struct Node
{
	int number;
	struct Node* next;
};

//指向鏈表第一個節點的指針
struct Node* head = NULL;

// 生產者函數、
void* producer(void* arg)
{
	while(1)
	{
		//創建新的鏈表節點
		pthread_mutex_lock(&mutex);
		struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
		pnew->next = head;
		head = pnew;
		pnew->number = rand() % 1000;
		printf("add+++ node, number: %d, tid = %ld\n", pnew->number, pthread_self());
		pthread_mutex_unlock(&mutex);

		//生產者生產了東西,通知消費者消費
		pthread_cond_signal(&cond);
	}
	return NULL;
}

//消費者函數
void* customer(void* arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);
		while (head == NULL)
		{
			//鏈表爲空,阻塞
			pthread_cond_wait(&cond, &mutex);
		}

		struct Node* pnode = head;
		head = head->next;
		printf("del--- node, number: %d, tid = %ld\n", pnode->number, pthread_self());
		free(pnode);
		pthread_mutex_unlock(&mutex);
	}

	return NULL;
}

int main(int argc, char *argv[])
{
	pthread_t ptid[5], ctid[5];
	pthread_cond_init(&cond,NULL);
	pthread_mutex_init(&mutex,NULL);

	for (int i=0; i<5; ++i)
	{
		pthread_create(&ptid[i], NULL, producer, NULL);
		pthread_create(&ctid[i], NULL, customer, NULL);
	}

	for (int i=0; i<5; ++i)
	{
		pthread_join(ptid[i], NULL);
		pthread_join(ctid[i], NULL);
	}
	pthread_cond_destroy(&cond);
	pthread_mutex_destroy(&mutex);
	return 0;
}

 

6. 信號量

  • 信號量用在多線程多任務同步的,一個線程完成了某一個動作就通過信號量告訴別的線程,別的線程再進行某些動作。
  • 信號量不一定是鎖定某一個資源,而是流程上的概念,比如:有A,B兩個線程,B線程要等A線程完成某一任務以後再進行自己下面的步驟,這個任務 並不一定是鎖定某一資源,還可以是進行一些計算或者數據處理之類。
  • 信號量(信號燈)與互斥鎖和條件變量的主要不同在於”燈”的概念,燈亮則意味着資源可用,燈滅則意味着不可用
  • 信號量主要阻塞線程, 不能完全保證線程安全.
  •  如果要保證線程安全, 需要信號量和互斥鎖一起使用.

 

- 信號量類型:

sem_t
  在這個變量中記錄了一個整形數, 如果這個數據 是5, 允許有五個線程訪問數據
          o o o o o
          如果有一線程訪問了共享資源, 這個整形數 -1, 後邊又有4個線程訪問了共享數據 0, 
          這時候, 再有線程訪問共享數據, 這些線程阻塞

- 信號量操作函數:

#include <semaphore.h>
  // 初始化信號量
  int sem_init(sem_t *sem, int pshared, unsigned int value);
  	參數: 
  		- sem: 信號量的地址
  		- pshared: 0-> 處理線程, 1-> 處理進程
  		- value: sem_t中整形數初始化

  // 釋放資源
  int sem_destroy(sem_t *sem);

  // 有可能引起阻塞
  // 調用一次這個函數 sem 中整形數 --
  // 當 sem_wait 並且 sem中的整形數爲0 , 阻塞了
  int sem_wait(sem_t *sem);

  // 當 sem_trywait 並且 sem中的整形數爲0 , 返回, 不阻塞
  int sem_trywait(sem_t *sem);

  // 當 sem_timedwait 並且 sem中的整形數爲0 , 阻塞一定的時長, 時間到達, 返回
  int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

  // 當 sem_post sem 中的整形數 ++
  int sem_post(sem_t *sem);

  // 查看 sem中的整形數的值, 通過第二個參數返回
  int sem_getvalue(sem_t *sem, int *sval);

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章