互斥量(mutex)
很多變量需要在線程間共享,這個變量就稱爲共享變量,可以通過共享數據完成線程之間的交互.
但是,多個線程併發的操作共享變量就會出現問題.
如下模擬實現一個網上購票系統:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100; //表示當前的票有多少張
void* BuyTicket(void* arg)
{
char* s = (char*)arg;
while(1)
{
if(ticket > 0)
{
usleep(1000);
printf("%s buy ticket, %d\n",s,ticket);
--ticket;
}
else
{
break;
}
}
return NULL;
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1,NULL,BuyTicket,(void*)"thread 1");
pthread_create(&t2,NULL,BuyTicket,(void*)"thread 2");
pthread_create(&t3,NULL,BuyTicket,(void*)"thread 3");
pthread_create(&t4,NULL,BuyTicket,(void*)"thread 4");
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_join(t4,NULL);
return 0;
}
演示出現錯誤的結果:
出現以上情況的原因:
- if語句判斷爲真以後,代碼可以併發的切換到其他的線程
- usleep這個是模擬漫長的業務過程,在這個業務過程中,就可能有多個線程進入該代碼段
- --ticket本就不是一個原子操作
要解決上面的問題,需要做到以下三點:
- 代碼必須要有互斥行爲: 當代碼進入臨界區執行的時候,不允許其他線程進入該臨界區
- 如果多個線程的代碼同時要求執行臨界區的代碼,並且臨界區沒有線程在執行,那麼只允許一個線程進入臨界區
- 如果線程不在臨界區執行,那麼該線程不能阻止其他線程進入臨界區
所以就此引入我們的互斥量
互斥量接口
初始化互斥量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
int pthread_mutex_init(pyhread_mutex_t* restrict mutex,const pthread_mutexattr_t* restrict attr);
參數:
mutex: 要初始化的互斥量
attr:填爲NULL即可
銷燬互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:
- 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要銷燬
- 不要銷燬一個已經加鎖的互斥量
- 已經銷燬的互斥量,要確保後面不會有線程再嘗試加鎖
互斥量的加鎖和解鎖
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:
成功返回0,失敗返回錯誤號
調用 pthread_lock 時可能會出現以下情況:
- 互斥量處於未加鎖狀態,那麼就會對這個互斥量加鎖,同時返回成功
- 如果這個互斥量已經加鎖,這是 pthread_lock 就會進入阻塞狀態,等待互斥量解鎖
根據互斥量改進上面的購票系統:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t g_lock; //創建一個互斥量
int ticket = 100; //表示當前的票有多少張
void* BuyTicket(void* arg)
{
char* s = (char*)arg;
while(1)
{
pthread_mutex_lock(&g_lock); //加鎖
if(ticket > 0)
{
usleep(100);
printf("%s buy ticket, %d\n",s,ticket);
--ticket;
pthread_mutex_unlock(&g_lock); //解鎖
}
else
{
pthread_mutex_unlock(&g_lock); //解鎖
break;
}
}
return NULL;
}
int main()
{
pthread_mutex_init(&g_lock,NULL); //初始化互斥量
pthread_t t1,t2,t3,t4;
pthread_create(&t1,NULL,BuyTicket,(void*)"thread 1");
pthread_create(&t2,NULL,BuyTicket,(void*)"thread 2");
pthread_create(&t3,NULL,BuyTicket,(void*)"thread 3");
pthread_create(&t4,NULL,BuyTicket,(void*)"thread 4");
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_join(t4,NULL);
pthread_mutex_destroy(&g_lock); //銷燬互斥量
return 0;
}
死鎖
如果在上面的代碼中,加了鎖以後沒有解鎖,會出現什麼問題呢?
比內存泄露更可怕的事情!!! 死鎖!!!
出現內存泄露, 對於一個256G內存的服務器來說, 往往需要很久纔會將內存消耗殆盡. 而且企業中的服務 器往往會定期進行 "例行重啓".
出現死鎖, 系統中的某些線程會直接停止工作, 導致整個服務器的功能瞬間失效!!
死鎖的兩個常見場景:
- 一個線程獲取到鎖之後, 又嘗試獲取鎖, 就會出現死鎖.
- 兩個線程A和B. 線程A獲取了鎖1, 線程B獲取了鎖2. 然後A嘗試獲取鎖2, B嘗試獲取鎖1. 這個時候雙方都 無法拿到對方的鎖. 並且會在獲取鎖的函數中阻塞等待. 如果線程數和鎖的數目更多了, 就會使死鎖問題更容易出現, 問題場景更復雜. 對於這種需要獲取多個鎖的場景, 規定所有的線程都按照固定的順序來獲取鎖, 能夠一定程度上避免死鎖
總結成一句話就是,拿了鎖卻沒有及時釋放,就會產生死鎖
線程安全和可重入
可重入函數: 在多個執行流中被同時調用不會存在邏輯上的問題.
線程安全函數: 在多線程中被同時調用不會存在問題.
這兩個概念都是在描述函數在多個執行流中調用的情況.
- 線程安全 -> 線程
- 可重入函數 -> 線程&信號處理函數
因此可重入的要求比線程安全的要求要更嚴格!
- 可重入函數一般情況下都是線程安全的.
- 線程安全函數不一定是可重入的.
代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
pthread_mutex_t g_lock;
int g_count = 0;
//當前是線程安全,不可重入的
void Fun()
{
pthread_mutex_lock(&g_lock);
printf("lock\n");
++g_count;
sleep(2);
printf("unlock\n");
pthread_mutex_unlock(&g_lock);
}
void* ThreadEntry(void* arg)
{
(void)arg;
while(1)
{
Fun();
}
return NULL;
}
void MyHandler(int sig)
{
(void)sig;
Fun();
}
int main()
{
signal(SIGINT,MyHandler);
pthread_mutex_init(&g_lock,NULL);
pthread_t t1;
pthread_create(&t1,NULL,ThreadEntry,NULL);
pthread_join(t1,NULL);
ThreadEntry(NULL);
pthread_mutex_destroy(&g_lock);
return 0;
}
當前的執行流程爲:
- main函數創建線程,並且調用線程入口函數ThreadEntry.
線程入口函數ThreadEntry內部死循環的調用Fun()函數
- Fun()函數進行 加鎖 -> 操作 -> 解鎖
如果在線程入口函數執行到加鎖以後,解鎖之前我們按下了 ctrl+c 會發生什麼情況呢?
當我們在ThreadEntry調用lock以後(unlock以前)按下 ctrl+c,這是會觸發信號捕捉函數,在信號捕捉函數裏會去調用 Fun() 函數.
但是調用Fun()函數的第一步就是解鎖,這時鎖已經被ThreadEntry函數獲取,那麼我們的信號捕捉函數就無法獲取鎖,只能等待鎖被釋放再去執行.
但是,之前在信號捕捉時候提到過,在執行完信號捕捉函數以前,主函數會一直阻塞的等待,知道信號捕捉函數退出,在接着執行.
這時候就出現了 ThreadEntry 在等待 MyHandler執行完, MyHandler 函數在等待ThreadEntry函數釋放鎖.所以就阻塞了.
因此對於這個場景下, Fun()函數是線程安全的, 但是不可重入.