線程安全
文章目錄
一:線程的執行方式
- 線程之間就像是比賽時的起跑者,如果沒有裁判和比賽規則約束,誰都有可能搶跑.誰都有可能領先奪冠.
- 線程是搶佔式執行的,正是因爲這種搶佔式的執行方式.引來了線程安全問題
- 如下面的程序
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
//互斥鎖/互斥量
pthread_mutex_t mutex;
#define THREAD_NUM 2
int g_count=0;
void *ThreadEntry(void * arg){
(void) arg;
for(int i=0;i<500000;++i){
//如果當前鎖已經被其他線程獲取到了,
//當前線程再想獲取就會在lock函數處阻塞
pthread_mutex_lock(&mutex);
++g_count;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(){
pthread_mutex_init(&mutex,NULL);
pthread_t tid[THREAD_NUM];
for(int i=0; i < THREAD_NUM ; ++i){
pthread_create(&tid[i],NULL,ThreadEntry,NULL);
}
for(int i=0;i<THREAD_NUM;++i){
pthread_join(tid[i],NULL);
}
printf("g_count = %d \n",g_count);
pthread_mutex_destroy(&mutex);
return 0;
}
【執行結果】
【結果分析】
- 該程序創建了兩個線程,若線程安全的話得到的結果應該是2x500000,而不會出現這種情況
- 可以看到多次的執行效果都是不同的,那麼多次執行相同的程序爲什麼會出現不同的結果呢?
- 原因是線程的搶佔式執行引起的線程安全問題,
- 當線程A從內存中讀到數據進行計算時,沒等到線程A結束線程B就從內存中讀取了和線程A相同的數據,最後當兩個線程將結果寫入到內存時,發現兩者的執行結果是相同的所以只寫了一次.這樣導致程序在執行過程中好多次的執行過程都是"無用功".
1.線程安全相關知識
1.1臨界資源
- 臨界資源:多個線程執行共享的資源
- 臨界區:每個線程內部,訪問臨界資源的代碼,叫做 臨界區
- 互斥:任何時刻,互斥保證有且只有一個執行流進入臨界區,訪問臨界資源,通常對臨界區資源起到保護作用
- 原子性:不會被任何調度機制打斷的操作,該操作是有兩種狀態要麼完成,要麼沒完成
1.2.互斥量mutex
- 大部分情況,線程使用的數據都是局部數據,變量的地址空間在線程棧空間內,這樣的話變量就歸單個線程,其他線程無法獲取這種變量
- 但有時多個線程會共享一個變量,這樣的變量就是共享變量,可以通過數據的共享,完成線程之間的交互
1.3.線程鎖🔒的引入
- 由於線程搶佔式執行的特點,導致的問題如何解決呢?
做到這些需要解決三個問題
- 1.代碼必須要有互斥行爲,當代碼進入臨界區執行時,不允許其他線程進入該臨界區
- 2.如果多個線程同時要求執行臨界區的代碼,並且臨界區沒有線程在執行,那麼允許一個線程進入該臨界區
- 3.如果線程不在臨界區中執行,那麼該線程不能組織其他線程進入臨界區
要做到這三點,本質上就是需要將臨界區的資源鎖起來.Linux中提供的鎖叫做互斥量
2.解決線程的不安全問題
在臨界區中使用"互斥機制"就能解決線程不安全的問題
互斥鎖(單車道洗車車間門)
- 在自動洗車間洗車的時候,只有一輛車能夠通過這個洗車道.所以當第一輛車進入洗車間後,洗車間系統就將入口上鎖,其他的車只能在門外等待第一輛車洗車結束之後,才能進入洗車間洗車.這種加鎖的方式有效的避免了車與車之間的碰撞等問題。
- 那麼這個洗車間就相當於線程鎖的有效段(進入纔有效,出來就失效了) .當上一個線程拿到線程鎖後若他沒有釋放鎖,其他線程只能在線程鎖外等待着獲取鎖的資格。
【過程總結】
- 1.先加鎖
- 2.執行臨界區代碼
- 3.釋放鎖
同一時刻只能有一個線程獲取到鎖,只有這個獲取到鎖的線程才能執行臨界區的代碼,其他線程只能等待其釋放鎖之後纔有獲取鎖的權利
線程執行時先去獲取鎖,兩個線程同時執行.先獲取到鎖的線程先執行,後獲取鎖的線程等待獲取鎖.
待先獲取鎖的線程執行完後再對等待的線程加鎖後執行。
【總結】
- 爲了避免出現多個線程相互之間影響執行效果而設置的一種保障機制,使得先獲取到鎖的線程在不受外界影響的情況下執行臨界區的代碼.待其執行完之後.從而避免相互之間干擾的情況.
2.1.互斥量的接口
- 初始化互斥量的兩種方法
- 1.靜態分配
pthread_mutex_t mutex=PTHREAD_MUTEX_INITALIZER;
- 2.動態分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t*restrict attr);
- 銷燬互斥量
- 銷燬互斥量需要注意
- 使用pthread_mutex_initializer 初始化的互斥量不需要銷燬
- 不要銷燬一個已經加鎖的互斥量
- 已經銷燬 的互斥量,要確保之後不會有線程再嘗試加鎖
int pthread_mutex_destory(pthread_mutex_t *mutex);
- 互斥量加索和解鎖
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//返回值:成功返回0,失敗返回錯誤碼
- 互斥鎖 pthread_mutex 掛起等待鎖,一旦線程獲取鎖失敗,就會掛起(進入到操作系統提供的一個等待隊列中)
- 互斥鎖能夠保證線程安全,最終的程序效率會受到影響
- 除此之外,還有一個嚴重的問題–>死鎖
【代碼示例】
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
//互斥鎖/互斥量
pthread_mutex_t mutex;
#define THREAD_NUM 2
int g_count=0;
void *ThreadEntry(void * arg){
(void) arg;
for(int i=0;i<50000;++i){
//如果當前鎖已經被其他線程獲取到了,
//當前線程再想獲取就會在lock函數處阻塞
pthread_mutex_lock(&mutex);
++g_count;
pthread_mutex_unlock(&mutex);
//這個線程不會在其他線程釋放鎖之後立刻就能恢復執行
//而是在其他線程釋放鎖之後,由操作系統決定執行時間
}
return NULL;
}
int main(){
pthread_mutex_init(&mutex,NULL);
pthread_t tid[THREAD_NUM];
for(int i=0; i < THREAD_NUM ; ++i){
pthread_create(&tid[i],NULL,ThreadEntry,NULL);
}
for(int i=0;i<THREAD_NUM;++i){
pthread_join(tid[i],NULL);
}
printf("g_count = %d \n",g_count);
pthread_mutex_destroy(&mutex);
return 0;
}
2.2.死鎖的兩個場景
【死鎖簡介】
- 死鎖是指在一組進程中的各個進程均佔有不會釋放的資源,但因互相申請被其他進程所站用不會釋放的資源而處於的一種永久等待的狀態
【死鎖的四個必要條件】
- 互斥條件:一個資源每次只能被一個執行流使用
- 請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不放
- 不剝奪條件:一個執行流已獲得的資源,在末使用完之前,不能強行剝奪
- 循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源的關係
【情景一】
- 1.一個線程在加鎖後(沒有解鎖的情況下)再嘗試加鎖
- 即一個線程兩個鎖的現象
void *ThreadEntry(void * arg){
(void) arg;
for(int i=0;i<50000;++i){
pthread_mutex_lock(&mutex);
++g_count;
pthread_mutex_lock(&mutex);
//第一次獲取鎖之後沒有釋放就再次加鎖
pthread_mutex_unlock(&mutex);
}
return NULL;
}
【情景二】
- 2.兩個線程1、2有兩把鎖A、B.
.線程1先去獲取鎖A,再去獲取鎖B.同時線程2先去獲取鎖B ,再去獲取鎖A,也會死鎖.
【經典故事】
多個進程多把鎖的問題(哲學家吃飯)
- 哲學家的行爲(五根筷子五個人)
- 五個人五隻筷子,每個人拿起自己右手/左手邊的筷子.每個人只能拿到一隻筷子.(一隻筷子沒辦法吃雞).五個哲學家誰都不讓誰,每人拿着一根筷子僵持着.導致的結果是誰都吃不了雞.
如何解決這種尷尬場面(死鎖問題)呢?
比較實用的死鎖解決辦法(從代碼設計的角度來解絕死鎖問題)
- 1.短: 讓臨界區代碼儘量短
- 2.平: 臨界區代碼儘量不去調用其他複雜函數
- 3.快: 臨界區代碼執行速度儘量快,別做太耗時的操作
死鎖的解決辦法(針對哲學家就餐問題)
- 1.先給每根筷子編號,約定先拿編號小的筷子
這樣的約定是破除死鎖的常見辦法,破除死鎖中的環路條件
- 2.弄一個信號量(計數器),
申請資源的時候搞一個信號量,信號量記錄的是當前可用資源的個數
如果當前數值爲0了,申請資源操作就會等待
每個哲學家拿筷子的時候先進性P操作(其中的計數器記載的是可用資源的數目)
二:線程----->同步
【同步】:
- 線程是搶佔式執行的,所以沒有辦法控制次序.因此同步控制着線程與線程之間執行順序(主要還是搶佔式執行的結果,有時需要線程和線程之間按照一定的順序來執行)
【滑稽取錢】
- 一個人不離開ATM機一直取錢,一直佔用ATM機,讓外面的人一直等待着(線程餓死)
【同步】
-
取錢得時候ATM機沒錢了,佔用ATM機的滑稽只能退出機房在外等待,等到押炒員將錢拿過來再繼續取錢
-
線程鎖結束之後的情景
1.釋放鎖
2.等待條件就緒(1、2兩步操作必須是原子的,否則就會錯過其他線程的通知消息,會導致一直等待的情況發生)
3.重新獲取鎖,準備執行後續的操作
1.條件變量的使用
【簡單介紹】
- 當一個線程互斥地訪問某個變量時,它可能發現在其它線程改變狀態之前,它什麼也做不了,只能等待其他線程改變狀態。
- 例如一個線程訪問隊列時發現隊列爲空,它只能等待.只到其它線程將一個節點添加到隊列中,這種情況就需要用到條件變量。
簡單來說,對於條件變量函數來說.
只有在線程滿足某種特性的情況下才能
使用,否則會一直等待着
【條件變量函數】
- 初始化
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)
大部分的情況下,條件變量要搭配互斥鎖來使用
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
//互斥鎖/互斥量
pthread_mutex_t mutex;
pthread_cond_t cond;
int g_count=0;
void *ThreadEntry1(void * arg){
(void) arg;
while(1){
printf("傳球\n");
pthread_cond_signal(&cond);
//等待扣籃老哥的信號
usleep(789789);
}
return NULL;
}
void *ThreadEntry2(void *arg){
(void) arg;
while(1){
pthread_cond_wait(&cond,&mutex);
//TODO 搭配互斥鎖
//執行這個pthread_cond_wait 函數就會導致線程被阻塞
//阻塞到其他線程發送一個通知
printf("扣籃\n");
usleep(123456);
}
return NULL;
}
int main(){
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,ThreadEntry1,NULL);
pthread_create(&tid2,NULL,ThreadEntry2,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
2.生產者消費者模型
- 優點
- 解耦
- 支持併發
- 支持忙先不均
這是一個多線程場景中的典型應用,應用場景非常廣泛
分工協作,提高效率
生產者負責產生數據,把數據放到交易場所中
消費者負責消費數據,把數據從交易廠所中取走
- 消費者之間爲互斥關係(搶購一份東西)
- 生產者之間也爲互斥關係(提供一份商品)
- 生產者與消費者之間互斥同步關係
3.c++提供的用法
c++queue模擬阻塞隊列的生產消費模型
#include <iostream>
#include <queue>
#include <stdlib.h>
#include <pthread.h>
#define NUM 8
class BlockQueue{
private:
std::queue<int> q;
int cap;
pthread_mutex_t lock;
pthread_cond_t full;
pthread_cond_t empty;
private:
void LockQueue()
{
pthread_mutex_lock(&lock);
}
void UnLockQueue()
{
pthread_mutex_unlock(&lock);
}
void ProductWait()
{
pthread_cond_wait(&full, &lock);
}
void ConsumeWait()
{
pthread_cond_wait(&empty, &lock);
}
void NotifyProduct()
{
pthread_cond_signal(&full);
}
void NotifyConsume()
{
pthread_cond_signal(&empty);
}
bool IsEmpty()
{
return ( q.size() == 0 ? true : false );
}
bool IsFull()
{
return ( q.size() == cap ? true : false );
}
public:
BlockQueue(int _cap = NUM):cap(_cap)
{
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&full, NULL);
pthread_cond_init(&empty, NULL);
}
void PushData(const int &data)
{
LockQueue();
while(IsFull()){
NotifyConsume();
std::cout << "queue full, notify consume data, product stop." <<
std::endl;
ProductWait();
}
q.push(data);
// NotifyConsume();
UnLockQueue();
}
void PopData(int &data)
{
LockQueue();
while(IsEmpty()){
NotifyProduct();
std::cout << "queue empty, notify product data, consume stop." <<
std::endl;
ConsumeWait();
}
data = q.front();
q.pop();
// NotifyProduct();
UnLockQueue();
}
~BlockQueue()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&full);
pthread_cond_destroy(&empty);
}
};
void *consumer(void *arg) {
BlockQueue *bqp = (BlockQueue*)arg;
int data;
for( ; ; ){
bqp->PopData(data);
std::cout << "Consume data done : " << data << std::endl;
}
}
//more faster
void *producter(void *arg) {
BlockQueue *bqp = (BlockQueue*)arg;
srand((unsigned long)time(NULL));
for( ; ; ){
int data = rand() % 1024;
bqp->PushData(data);
std::cout << "Prodoct data done: " << data << std::endl;
// sleep(1);
}
}
int main()
{
BlockQueue bq;
pthread_t c,p;
pthread_create(&c, NULL, consumer, (void*)&bq);
pthread_create(&p, NULL, producter, (void*)&bq);
pthread_join(c, NULL);
pthread_join(p, NULL);
return 0; }
- 運行結果
三.線程池(頻繁申請和銷燬大量空間)
/*threadpool.h*/
/* 線程池:
* 一種線程使用模式。線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池維護着多個線
程,等待着監督管理者分配可併發執行的任務。這避免了在處理短時間任務時創建與銷燬線程的代價。線程池不僅能夠
保證內核的充分利用,還能防止過分調度。可用線程數量應該取決於可用的併發處理器、處理器內核、內存、網絡
sockets等的數量。
* 線程池的應用場景:
* 1. 需要大量的線程來完成任務,且完成任務的時間比較短。 WEB服務器完成網頁請求這樣的任務,使用線
程池技術是非常合適的。因爲單個任務小,而任務數量巨大,你可以想象一個熱門網站的點擊次數。 但對於長時間的任
務,比如一個Telnet連接請求,線程池的優點就不明顯了。因爲Telnet會話時間比線程的創建時間大多了。
* 2. 對性能要求苛刻的應用,比如要求服務器迅速響應客戶請求。
* 3. 接受突發性的大量請求,但不至於使服務器因此產生大量線程的應用。突發性大量客戶請求,在沒有線
程池情況下,將產生大量線程,雖然理論上大部分操作系統線程數目最大值不是問題,短時間內產生大量線程可能使內
存到達極限,出現錯誤.
* 線程池的種類:
* 線程池示例:
* 1. 創建固定數量線程池,循環從任務隊列中獲取任務對象,
* 2. 獲取到任務對象後,執行任務對象中的任務接口
* /*threadpool.hpp*/
#ifndef __M_TP_H__
#define __M_TP_H__
#include <iostream>
#include <queue>
#include <pthread.h>
#define MAX_THREAD 5
typedef bool (*handler_t)(int);
class ThreadTask
{
private:
int _data;
handler_t _handler;
public:
ThreadTask()
:_data(-1)
, _handler(NULL)
{}
ThreadTask(int data, handler_t handler) {
_data= data;
_handler = handler;
}
void SetTask(int data, handler_t handler) {
_data = data;
_handler = handler;
}
void Run() {
_handler(_data);
}
};
class ThreadPool
{
private:
int _thread_max;
int _thread_cur;
bool _tp_quit;
std::queue<ThreadTask *> _task_queue;
pthread_mutex_t _lock;
pthread_cond_t _cond;
private:
void LockQueue() {
pthread_mutex_lock(&_lock);
}
void UnLockQueue() {
pthread_mutex_unlock(&_lock);
}
void WakeUpOne() {
pthread_cond_signal(&_cond);
}
void WakeUpAll() {
pthread_cond_broadcast(&_cond);
}
void ThreadQuit() {
_thread_cur--;
UnLockQueue();
pthread_exit(NULL);
}
void ThreadWait(){
if (_tp_quit) {
ThreadQuit();
}
pthread_cond_wait(&_cond, &_lock);
}
bool IsEmpty() {
return _task_queue.empty();
}
static void *thr_start(void *arg) {
ThreadPool *tp = (ThreadPool*)arg;
while(1) {
tp->LockQueue();
while(tp->IsEmpty()) {
tp->ThreadWait();
}
ThreadTask *tt;
tp->PopTask(&tt);
tp->UnLockQueue();
tt->Run();
delete tt;
}
return NULL;
}
public:
ThreadPool(int max=MAX_THREAD):_thread_max(max), _thread_cur(max),
_tp_quit(false) {
pthread_mutex_init(&_lock, NULL);
pthread_cond_init(&_cond, NULL);
}
~ThreadPool() {
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
bool PoolInit() {
pthread_t tid;
for (int i = 0; i < _thread_max; i++) {
int ret = pthread_create(&tid, NULL, thr_start, this);
if (ret != 0) {
std::cout<<"create pool thread error\n";
return false;
}
}
return true;
}
bool PushTask(ThreadTask *tt) {
LockQueue();
if (_tp_quit) {
UnLockQueue();
return false;
}
_task_queue.push(tt);
WakeUpOne();
UnLockQueue();
return true;
}
bool PopTask(ThreadTask **tt) {
*tt = _task_queue.front();
_task_queue.pop();
return true;
}
bool PoolQuit() {
LockQueue();
_tp_quit = true;
UnLockQueue();
while(_thread_cur > 0) {
WakeUpAll();
usleep(1000);
}
return true;
}
};
#endif
/*main.cpp*/
bool handler(int data)
{
srand(time(NULL));
int n = rand() % 5;
printf("Thread: %p Run Tast: %d--sleep %d sec\n", pthread_self(), data, n);
sleep(n);
return true; }
int main()
{
int i;
ThreadPool pool;
pool.PoolInit();
for (i = 0; i < 10; i++) {
ThreadTask *tt = new ThreadTask(i, handler);
pool.PushTask(tt);
}
pool.PoolQuit();
return 0;
}