Linux中的線程
- 什么是線程
- 線程的使用
- 原生線程庫
- 創建線程
- 線程的id
- 線程退出
- 等待線程join
- 分離線程
- 取消一個線程
- 線程的局部存儲
- 在c++程序中使用線程
- 使用c++自己封裝一個簡易的線程庫
- 線程互斥(多線程)
- 導致共享數據出錯的原因
- 互斥鎖
- 關鍵函數
- pthread_mutex_t :創建一個鎖
- pthread_mutex_init:初始化一個互斥鎖。
- pthread_mutex_lock:加鎖,如果鎖已被其他線程加鎖,則線程會阻塞直到鎖被釋放。
- pthread_mutex_unlock:釋放鎖,使其他等待的線程有機會獲得鎖。
- pthread_mutex_destroy:銷毀一個互斥鎖。
- 加鎖注意事項
- 使用C++自己封裝一個線程鎖
- 線程死鎖
- 什么是死鎖
- 死鎖的四個條件
- 死鎖的代碼實例
- 線程同步
- 什么是線程同步,作用是什么
- 條件變量
- 條件變量的使用
- 創建一個條件變量 pthread_cond_t
- 初始化條件變量 pthread_cond_init
- 等待條件變量 pthread_cond_wait
- 發送信號 pthread_cond_signal
- 喚醒所有信號 pthread_cond_broadcast
- 銷毀條件變量 pthread_cond_destroy
- 生產者消費者模型
- 偽喚醒
- 信號量
- sem_init:初始化一個信號量。
- sem_destroy:銷毀一個信號量。
- sem_wait:等待信號量。
- sem_post:釋放信號量.
- sem_trywait:嘗試等待信號量。
- 信號量實現環形隊列
什么是線程
線程是操作系統能夠進行運算調度的最小單位,被包含在進程之中,是進程中的實際運作單位。在進程的學習中,我們了解到一個可執行文件可以執行多個進程,而線程則是把進程所執行的任務可以再細分成一個或多個執行流來交給CPU執行.
圖片來自必應搜索
Linux系統中沒有真正意義上的線程,它是由進程的PCB來模擬的線程,被統一稱為輕量級進程(Light weight process) ,因此在底層中,CPU調度的還是一個一個的進程,只不過是這些進程都是輕量級的進程,這樣CPU調度起來也更方便,不用再進行數據的轉換,調度的還是跟以前一樣的進程而已,大概圖解如下:
圖片來自必應搜索
之前學習的進程實際上就是單線程的進程,在后續的Linux學習中,完全可以把進程看成包含一個或多個線程(即輕量級進程)
線程的使用
linux系統中查詢線程pid:
ps -aL
原生線程庫
想要操作進程,那必須使用系統提供的接口,線程也是一樣的,而原生線程庫定義了操作系統應該提供的一組API,以支持線程創建、同步、通信和控制等功能。
這個庫一般是名為pthread的庫,該庫提供了創建和管理線程所需的函數。因此,我們在對線程進行操作時,一般需要包含頭文件 pthread.h
并且,因為是外部庫,因此在編譯的時候應該找到該庫然后添加相應的編譯條件進行編譯例如:
g++ -o mythread mythread.cc -std=c++11 -lpthread
創建線程
pthread_create()
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
參數說明:
thread:這是一個指向 pthread_t 類型變量的指針,該變量用于保存新創建線程的標識符。
attr:這是一個指向 pthread_attr_t 類型變量的指針,它允許程序員設置新線程的屬性,如棧大小、線程優先級等。如果 attr 為 NULL,將使用默認屬性。一般都是設置為空
start_routine:這是一個指向函數的指針,該函數將作為新線程的入口點。相當于這個線程需要執行的方法
arg:這是傳遞給 start_routine 函數的參數。
返回值:
如果線程創建成功,pthread_create 返回 0。
如果在創建線程時發生錯誤,將返回錯誤碼。
下面是一個創建線程并讓線程執行某個方法的代碼示例:
#include<iostream>
#include<pthread.h>void* threadRotine(void* arg)
{std::string name = static_cast<char*>(arg);std::cout<<"i am "<<name<<" my pthreadid: "<<pthread_self()<<std::endl;
}int main()
{pthread_t tid;int status;pthread_create(&tid,nullptr,&threadRotine,(void*)"thread-1");status = pthread_join(tid,nullptr);return 0;
}
線程的id
pthread_self();函數返回調用線程的線程ID。
pthread_t my_thread_id = pthread_self();
線程退出
退出一個線程可以讓線程執行完任務后自行返回,也可以使用線程終止函數
pthread_exit()用于線程的退出
void* thread_function(void* arg) {// 執行任務pthread_exit(NULL); // 線程退出
}
可以在線程執行方法結束后調用以結束線程,但是不能用exit()函數,因為exit()是進程的退出函數
等待線程join
線程默認是要被主線程等待的,否則會導致類似進程的僵尸問題
pthread_join()用于等待一個線程終止并獲取其退出狀態。
這個函數是需要確保主線程在子線程完成其工作之后才繼續執行時進行調用。
int pthread_join(pthread_id pthread,void **retval);//
參數說明:
pthread:要等待其終止的線程的線程標識符。
retval:指向 void 指針的指針,用于接收線程退出時的狀態信息。
例如下面的代碼中就封裝了一個線程信息返回的類,并利用pthread_join函數拿到其返回值并進行信息的打印:
#include<iostream>
#include<pthread.h>//封裝線程返回信息的類
class PthreadReturn
{
public:PthreadReturn(pthread_t id,const std::string& info,int code):_id(id),_info(info),_code(code){}
public:pthread_t _id;std::string _info;int _code;
};void* threadRotine(void* arg)
{std::string name = static_cast<char*>(arg);std::cout<<"i am "<<name<<" my pthreadid: "<<pthread_self()<<std::endl;//對返回信息進行傳參PthreadReturn* ret = new PthreadReturn(pthread_self(),"thread 1",10);return ret;
}int main()
{pthread_t tid;int status;pthread_create(&tid,nullptr,&threadRotine,(void*)"thread-1");void* ret = nullptr;status = pthread_join(tid,&ret); //拿到返回信息retPthreadReturn* p = static_cast<PthreadReturn *>(ret); //因為返回信息的參數是void** ,因此在這里需要將類型還原成類std::cout<<p->_id<<","<<p->_info<<","<<p->_code<<std::endl;delete p;return 0;
}
分離線程
線程一旦分離出去,就和當前進程沒有任何關系,即便退出了也會被系統回收.
但是一般建議任何程序都以主線程結束
pthread_detach()
pthread_detach(thread_id);
一個線程要么是jion的,要么是detach的,默認是jionable的
取消一個線程
pthread_cancal()
pthread_cancel(thread_id);
如果線程已經被分離了,那么這個線程就可以被取消但不能join
線程的局部存儲
__thread 關鍵字
__thread 類型 變量名;
使用__thread關鍵字定義的變量就相當于給每個線程都定義了這個變量,因此每個線程在使用這個變量的時候都是單獨的,并不是全局變量
在c++程序中使用線程
因為linux中的線程庫也是封裝的,c++也提供了對這個庫的封裝,使用線程的頭文件thread,當然,因為這是c++封裝的pthread,因此底層依然是調用了pthread_create等函數,因此在編譯的時候在編譯條件那里依然是要加上 -lprhead
g++ -o $@ $^ -std=c++11 -lpthread
下面用C++提供的線程庫來寫一個線程相關的示例:
#include<iostream>
#include<thread>using namespace std;void myrun()
{cout<<"i am a thread"<<endl;
}int main()
{thread t(myrun);t.join();return 0;
}
當然,因為C++這個線程頭文件本身也是對pthread.h庫的封裝,因此我們也可以自己封裝一個簡易的線程庫
使用c++自己封裝一個簡易的線程庫
Makefile文件: //因為是對pthread.h庫進行的封裝,因此在編譯的時候還是需要鏈接庫
Mypthread:Mypthread.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm Mypthread
Thread.hpp
#pragma once#include<iostream>
#include<pthread.h>
#include<functional>using namespace std;template<class T>
using func_t = function<void(T)>;template<class T>
class Thread
{
public:Thread(func_t<T> func,const string& threadname,T data):_tid(0),_ThreadName(threadname),_isruning(false),_func(func),_data(data){}static void* threadRoutine(void* arg){Thread* t = static_cast<Thread*>(arg);t->_func(t->_data);return nullptr;}bool Start(){pthread_t id;int n = pthread_create(&id,nullptr,threadRoutine,this);if(n == 0){_isruning = true;return true;}else{return false;}}string ThreadName(){return _ThreadName;}bool join(){if(!_isruning) return false;void* ret = nullptr;int n = pthread_join(pthread_self(),&ret);if(n == 0){_isruning = false;return true;}return false;}bool IsRun(){return _isruning;}~Thread(){}private:pthread_t _tid;string _ThreadName;bool _isruning;func_t<T> _func;T _data;
};
Mythread.cc
#include<iostream>
#include"Thread.hpp"
#include<vector>using namespace std;string getThreadname()
{char nums[64];static int num = 1;snprintf(nums,sizeof(nums),"Thread-%d",num++);return nums;
}void myfunc(void* arg)
{cout<<"i am a thread"<<endl;
}int main()
{vector<Thread<void*>> vt;int num = 5;for(int i = 0 ; i < num ; i++){vt.push_back(Thread<void*>(myfunc,getThreadname(),nullptr));}for(auto & e:vt){cout<<" thread_name: "<<e.ThreadName()<<" is thread_run?: "<<e.IsRun()<<endl;}cout<<"Start:"<<endl;for(auto & e:vt){e.Start();cout<<" thread_name: "<<e.ThreadName()<<" is thread_run?: "<<e.IsRun()<<endl;}cout<<"join:"<<endl;for(auto & e:vt){e.join();cout<<" thread_name: "<<e.ThreadName()<<" is thread_run?: "<<e.IsRun()<<endl;}//Thread tid(myfunc,getThreadname());// cout<<"is runing?"<<tid.IsRun()<<" id: "<<tid.Pthread_id()<<endl;// tid.Start();// cout<<"is runing?"<<tid.IsRun()<<" id: "<<tid.Pthread_id()<<endl;// tid.join();// cout<<"is runing?"<<tid.IsRun()<<" id: "<<tid.Pthread_id()<<endl;return 0;
}
線程互斥(多線程)
當我們創建了多個線程,并且多個線程在對全局變量或者共享區數據進行訪問并更改時,可能會出現一些意想不到的問題.
例如,在上面.hpp文件的基礎上,寫了一個簡易的模仿搶票的小程序,在這個程序中有5個線程對1000張票進行搶票的一個動作,每有一個程序搶到一張票,總票數就-1 ,直到總票數為0
運行以下代碼:
#include<iostream>
#include"Thread.hpp"
#include<vector>
#include<unistd.h>
#include<cstdio>using namespace std;string getThreadname()
{char nums[64];static int num = 1;snprintf(nums,sizeof(nums),"Thread-%d",num++);return nums;
}int ticket = 1000; //全局共享資源void getTicket(string name)
{while(true){if(ticket > 0){usleep(1000);printf("%s get a ticket :%d \n",name.c_str(),ticket);ticket--;}else{break;}}
}int main()
{//pthread_mutex_t mutex;//pthread_mutex_init(&mutex,nullptr);string name1 = getThreadname();Thread<string> t1(getTicket,name1,name1);string name2 = getThreadname();Thread<string> t2(getTicket,name2,name2);string name3 = getThreadname();Thread<string> t3(getTicket,name3,name3);string name4 = getThreadname();Thread<string> t4(getTicket,name4,name4);string name5 = getThreadname();Thread<string> t5(getTicket,name5,name5);t1.Start();t2.Start();t3.Start();t4.Start();t5.Start();
// 添加延遲以確保線程有機會執行sleep(3);t1.Join();t2.Join();t3.Join();t4.Join();t5.Join();return 0;
}
這里我們創建了5個線程,分別代表要去看演唱會搶票的人,大家分別搶票,搶到一張票共享數據ticket就 – ,直到ticket為0,但是,運行結果如下:
導致共享數據出錯的原因
因為 線程的時間片輪轉+寄存器的逐步訪問 ,這才導致本應該為0的時候就結束搶票的,但是卻搶出了負數這樣的bug,這是因為以下幾步:
- 當前線程對自己的線程TCB中所保存票數進行判斷
- 將自己線程內存中的數據放到寄存器
- 寄存器更改
- 再把寄存器的數據交給線程內存進行保存
但是一頓操作下來,到其中的某一個步驟的時候,自己的線程時間片到了,CPU直接從當前步驟中斷執行下一個線程,這樣的話下一個線程已經對共享數據做出更改的時候當前線程卻還記錄的是自己的數據,這樣的話就沒法做到對共享數據的同步
寄存器的逐步訪問解釋圖(來自必應搜索)
因此當我們有了線程互斥鎖的概念
互斥鎖
互斥鎖是一種基本的同步機制,用于保護共享資源,確保同一時間只有一個線程可以訪問。
關鍵函數
因為是線程的鎖,因此還是需要用到頭文件 <pthread.h>
pthread_mutex_t :創建一個鎖
類型通常用于聲明互斥鎖變量例如:
pthread_mutex_t mutex
當前就已經創建了一個鎖
pthread_mutex_init:初始化一個互斥鎖。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
參數:
mutex:指向將要被初始化的互斥鎖的指針。
attr:指向互斥鎖屬性的指針。如果傳入NULL,將使用默認屬性。
返回值:
成功時返回0;
失敗時返回錯誤代碼。
使用此函數初始化鎖是多線程編程中的標準做法,以確保鎖在使用前已正確設置。
pthread_mutex_lock:加鎖,如果鎖已被其他線程加鎖,則線程會阻塞直到鎖被釋放。
int pthread_mutex_lock(pthread_mutex_t *mutex);
參數:
mutex:指向需要加鎖的互斥鎖的指針。
返回值:
成功時返回0;
失敗時返回錯誤代碼。
這個函數是實現線程安全的關鍵,用于保護臨界區(即共享代碼區),確保同一時間只有一個線程可以執行臨界區代碼。
pthread_mutex_unlock:釋放鎖,使其他等待的線程有機會獲得鎖。
當一個線程完成其對共享資源的操作后,它應調用此函數來解鎖,使其他阻塞(等待這個鎖釋放的)線程可以繼續執行。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
參數:
mutex:指向需要解鎖的互斥鎖的指針。
返回值:
成功時返回0;
失敗時返回錯誤代碼。
pthread_mutex_destroy:銷毀一個互斥鎖。
當互斥鎖不再被使用時,應該調用此函數來釋放與互斥鎖相關的資源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
參數:
mutex:指向需要銷毀的互斥鎖的指針。
返回值:
成功時返回0;
失敗時返回錯誤代碼。
銷毀互斥鎖是資源回收的重要步驟,避免內存泄漏。
加鎖注意事項
1.盡可能的少給代碼加鎖,因為加鎖會讓線程在執行某段代碼的時候由并行轉為串行,會影響效率
2. 一般加鎖都是給臨界區加鎖
3. 申請鎖都是程序員自己保證的,因此要格外注意內存泄漏的問題
4. 誰加鎖,誰解鎖
下面是加了鎖,更改后的代碼:
#include<iostream>
#include"Thread.hpp"
#include<vector>
#include<unistd.h>
#include<cstdio>using namespace std;string getThreadname()
{char nums[64];static int num = 1;snprintf(nums,sizeof(nums),"Thread-%d",num++);return nums;
}int ticket = 1000; //全局共享資源void getTicket(pthread_mutex_t* mutex)
{while(true){//加鎖pthread_mutex_lock(mutex);if(ticket > 0){usleep(1000);printf(" get a ticket :%d \n",ticket);ticket--;}else{ //解鎖pthread_mutex_unlock(mutex);break;}//解鎖pthread_mutex_unlock(mutex);}
}int main()
{//創建鎖pthread_mutex_t mutex;pthread_mutex_init(&mutex,nullptr);string name1 = getThreadname();Thread<pthread_mutex_t*> t1(getTicket,name1,&mutex);string name2 = getThreadname();Thread<pthread_mutex_t*> t2(getTicket,name2,&mutex);string name3 = getThreadname();Thread<pthread_mutex_t*> t3(getTicket,name3,&mutex);string name4 = getThreadname();Thread<pthread_mutex_t*> t4(getTicket,name4,&mutex);string name5 = getThreadname();Thread<pthread_mutex_t*> t5(getTicket,name5,&mutex);t1.Start();t2.Start();t3.Start();t4.Start();t5.Start();
// 添加延遲以確保線程有機會執行sleep(3);t1.Join();t2.Join();t3.Join();t4.Join();t5.Join();//銷毀鎖pthread_mutex_destroy(&mutex);return 0;
}
這樣的話這個簡單的搶票程序就沒問題了
使用C++自己封裝一個線程鎖
封裝這個鎖的目的是為了更方便的對臨界區的代碼進行管理,依舊是上方的搶票代碼案例:
添加.hpp文件LockGuard.hpp 因為是對鎖所做封裝,因此還是需要用到pthread.h頭文件
#pragma once #include<pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t* mutex):_mutex(mutex){}void Lock(){pthread_mutex_lock(_mutex);}void Unlock(){pthread_mutex_unlock(_mutex);}~Mutex(){}
private:pthread_mutex_t* _mutex;
};class Guard
{
public:Guard(pthread_mutex_t* lock):_mutex(lock){_mutex.Lock();}~Guard(){_mutex.Unlock();}
public:Mutex _mutex;
};
這樣寫的目的是讓我們定義一個鎖之后除了對應作用域可以自行解鎖和銷毀而不用手動的去釋放,并且可以根據需要對臨界區和非臨界區代碼使用{}來分割
void getTicketname(string name)
{while(true){//非臨界區代碼//......//臨界區代碼塊 用{}分割{//加鎖:Guard Mutex(&mutex);if(ticket > 0){usleep(1000);printf("%s get a ticket :%d \n",name.c_str(),ticket);ticket--;}else{ break;}}}
}
線程死鎖
什么是死鎖
在多線程環境中,當兩個或多個線程相互等待對方釋放資源,從而無限期地阻塞彼此的進程,就會發生死鎖。這些資源可以是任何東西,如數據、文件或任何由互斥鎖保護的資源。
死鎖的四個條件
產生死鎖的四個必要條件:
- 互斥條件:資源至少有一個不能被共享,只能由一個線程占用。
- 持有并等待條件:一個線程至少持有一個資源,并等待獲取一個當前被其他線程持有的資源。
- 非搶占條件:資源不能被強制從一個線程中搶占,只能由持有資源的線程主動釋放。
- 循環等待條件:涉及的線程之間形成一個環路,每個線程都在等待下一個線程持有的資源。
只要沒滿足上面的四條,那就都不是死鎖,因此,想要不發生死鎖,只需要破壞上面四條中的任意一條即可
死鎖的代碼實例
下面的例子中,兩個線程嘗試獲取兩把鎖,從而導致死鎖:
pthread_mutex_t lock1, lock2;void* thread1(void* arg) {pthread_mutex_lock(&lock1);sleep(1); // 確保線程2能鎖住lock2pthread_mutex_lock(&lock2);// 執行任務...pthread_mutex_unlock(&lock2);pthread_mutex_unlock(&lock1);return NULL;
}void* thread2(void* arg) {pthread_mutex_lock(&lock2);sleep(1); // 確保線程1能鎖住lock1pthread_mutex_lock(&lock1);// 執行任務...pthread_mutex_unlock(&lock1);pthread_mutex_unlock(&lock2);return NULL;
}
線程同步
什么是線程同步,作用是什么
多線程環境下,線程往往需要讀取或修改共享數據。如果對這些共享資源的訪問不加以控制,多個線程可能會同時修改同一資源,導致數據的不一致性。例如,當兩個線程同時更新同一個賬戶余額時,如果沒有適當的同步措施,最終的賬戶余額可能會出錯.因此,在臨界資源使用安全的前提下,讓多線程執行具有一定的順序性,這樣做是為了讓CPU資源能夠更加充分的被利用,這樣的情況被稱為線程同步.
線程同步是一種機制,它確保兩個或更多并發執行的線程在訪問共享資源時不會產生沖突。無論是在多核還是單核處理器上,線程同步都是必須的,以避免由于資源競爭引起的數據不一致或應用崩潰等問題。
條件變量
條件變量是用來自動阻塞一個線程,直到某特定條件為真為止。條件變量需要與互斥鎖(Mutex)一起工作,以避免競爭條件的發生。
它可以使線程在等待某個條件成立時進入阻塞狀態,一旦條件成立,條件變量就會喚醒一個或多個等待的線程。
條件變量的使用
創建一個條件變量 pthread_cond_t
與線程鎖一樣,想要對條件變量進行操作,首先得有這么個東西才行,類型為: pthread_cond_t :
pthread_cond_t cond;
初始化條件變量 pthread_cond_init
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
參數:
cond:指向將要被初始化的條件變量。
attr:指定條件變量屬性的指針,通常設置為NULL表示默認屬性。
返回值:成功返回0;失敗返回錯誤號。
也可以使用以下代碼直接 創建+初始化 全局的條件變量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
等待條件變量 pthread_cond_wait
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
參數:
cond:指向等待的條件變量。
mutex:與條件變量一起使用的互斥鎖,調用時必須已被當前線程鎖定。
返回值:成功返回0;失敗返回錯誤號。
此函數會釋放互斥鎖并等待條件變量被觸發,觸發后重新獲得互斥鎖繼續執行。 即當一個線程沒有收到能夠運行的信號時,就會從這里跳轉到等待隊列,等待能夠重新拿到互斥鎖再繼續運行自己的代碼
發送信號 pthread_cond_signal
int pthread_cond_signal(pthread_cond_t *cond);
參數:
cond:要觸發的條件變量。
返回值:成功返回0;失敗返回錯誤號。
此函數喚醒至少一個等待(被阻塞)在指定條件變量上的線程。即告訴那個線程,你要運行的條件已經達到了,可以運行了.
喚醒所有信號 pthread_cond_broadcast
int pthread_cond_broadcast(pthread_cond_t *cond);
cond:要觸發的條件變量。
返回值:成功返回0;失敗返回錯誤號。
此函數喚醒所有等待在指定條件變量上的線程。
銷毀條件變量 pthread_cond_destroy
int pthread_cond_destroy(pthread_cond_t *cond);
參數:
cond:要銷毀的條件變量。
返回值:成功返回0;失敗返回錯誤號。
生產者消費者模型
生產者和消費者模型是計算機領域中常用的一種資源控制方法,一般情況下,在一個完整的運行過程中,不可能只有單方面的生產者或者消費者的一方,基本都是一邊生產資源,一邊要拿資源,因此,總結下來生產者消費者模型如下:
- 生產者 和 消費者 間存在 競爭 和 互斥 的關系
- 消費者 和 消費者 間存在 競爭 和 互斥 的關系
- 生產 和 消費 這兩個行為之間存在 互斥 和 同步 的關系
下面是用C++封裝了一個生產者消費者模型:
BlockQueue.hpp
#pragma once#include <iostream>
#include <pthread.h>
#include <queue>int capdefault = 5;template <class T>
class BlockQueue
{
public:BlockQueue(int cap = capdefault) : _capacity(cap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_c, nullptr);pthread_cond_init(&_p, nullptr);}bool IsFull(){return _q.size() == _capacity;}void Push(const T &in) // 生產者{pthread_mutex_lock(&_mutex);while (IsFull()) // 使用循環檢查條件,防止偽喚醒{// 車位滿了,等小弟送車進來pthread_cond_wait(&_p, &_mutex);}_q.push(in);std::cout << "生產者生產了一個資源 :" << in << std::endl;// 生產者告訴消費者該消費了pthread_cond_signal(&_c);pthread_mutex_unlock(&_mutex);}bool IsEmpty(){return _q.empty();}void Pop(T *out) // 消費者{pthread_mutex_lock(&_mutex);while (IsEmpty()) // 使用循環檢查條件,防止偽喚醒{// 車庫沒車,等車送進來pthread_cond_wait(&_c, &_mutex);}*out = _q.front();_q.pop();std::cout << "消費者拿走了一個資源: " << *out << std::endl;// 消費者告訴生產者該生產了pthread_cond_signal(&_p);pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_c);pthread_cond_destroy(&_p);}private:std::queue<T> _q;pthread_mutex_t _mutex;pthread_cond_t _c;pthread_cond_t _p;int _capacity;
};
main函數所在文件 Main.cc
#include<iostream>
#include<string>
#include<ctime>
#include"BlockQueue.hpp"
#include"LockGuard.hpp"
#include<unistd.h>
#include<cstdio>using namespace std;void* productor(void* args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);//拿到車庫鑰匙,開始往車庫里送車while(true){//sleep(1);//創建數據作為車,把車送入車庫int data = rand() % 10 + 1 ;bq->Push(data);}
}void* consumer(void* args)
{BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);sleep(2);//拿到車庫鑰匙,開始把車挪走while(true){sleep(1);int data = 0 ;bq->Pop(&data);}}int main()
{srand((uint16_t)time(nullptr)^pthread_self());BlockQueue<int>* bq = new BlockQueue<int>(); pthread_t c , p;pthread_create(&c,nullptr,consumer,bq);pthread_create(&p,nullptr,productor,bq);pthread_join(c,nullptr);pthread_join(p,nullptr);return 0;
}
偽喚醒
在上述代碼的實現過程中,有一個小細節:
有一句判斷 if (IsFull())
void Push(const T &in) // 生產者{pthread_mutex_lock(&_mutex);if (IsFull()) {// 車位滿了,等小弟送車進來pthread_cond_wait(&_p, &_mutex);}_q.push(in);std::cout << "生產者生產了一個資源 :" << in << std::endl;// 生產者告訴消費者該消費了pthread_cond_signal(&_c);pthread_mutex_unlock(&_mutex);}
這里的if判斷可能在某些情況下造成偽喚醒
偽喚醒是指線程在等待條件變量時,即使沒有其他線程顯式地發出信號喚醒它,線程也會從等待狀態返回。換句話說,線程可能會在沒有滿足預期條件的情況下被喚醒。
比如,當生產者生產了一大堆資源,然后通知所有的進程過來拿數據(pthread_cand_broadcast),這樣的話就會有線程直接跳過判斷語句直接喚醒,從而造成風險
處理偽喚醒的正確方法是在等待條件變量返回后,始終重新檢查條件。通常的做法是在一個循環中使用條件變量,只有在條件滿足時才退出循環。這種模式通常被稱為“防偽喚醒模式”。
因此應該將代碼中的if 更改為 while
void Push(const T &in) // 生產者{pthread_mutex_lock(&_mutex);while (IsFull()) // 使用循環檢查條件,防止偽喚醒{// 車位滿了,等小弟送車進來pthread_cond_wait(&_p, &_mutex);}_q.push(in);std::cout << "生產者生產了一個資源 :" << in << std::endl;// 生產者告訴消費者該消費了pthread_cond_signal(&_c);pthread_mutex_unlock(&_mutex);}
這樣就正確處理了偽喚醒
信號量
信號量(Semaphore)是一種用于多線程同步和互斥的機制,是一個整數變量,它可以用來控制對共享資源的訪問。信號量主要分為兩類:二值信號量和計數信號量。
信號量是一個具有非負整數值的計數器,它支持兩種原子操作:
P操作(wait):如果信號量值大于零,則將其減一;如果信號量值為零,則阻塞直到信號量值大于零。
V操作(post):將信號量值加一,并喚醒一個等待在該信號量上的線程(如果有的話)。
要使用信號量需要加上頭文件 #include<semaphore.h>
信號量類型:sem_t ,用于聲明信號量變量。
sem_init:初始化一個信號量。
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:指向信號量對象的指針。
pshared:如果為0,信號量用于線程間同步;如果為非零,信號量用于進程間同步。
value:信號量的初始值。
返回值:成功返回0;失敗返回-1,并設置errno。
sem_destroy:銷毀一個信號量。
int sem_destroy(sem_t *sem);
sem:指向要銷毀的信號量對象的指針。
返回值:成功返回0;失敗返回-1,并設置errno。
sem_wait:等待信號量。
如果信號量值大于0,則將其減一;如果信號量值為0,則阻塞直到信號量值大于0。 這個函數通常用于封裝P()
int sem_wait(sem_t *sem);
sem:指向信號量對象的指針。
返回值:成功返回0;失敗返回-1,并設置errno。
sem_post:釋放信號量.
將信號量值加一,并喚醒一個等待在該信號量上的線程(如果有)。
這個函數通常用來封裝V()
int sem_post(sem_t *sem);
sem:指向信號量對象的指針。
返回值:成功返回0;失敗返回-1,并設置errno。
sem_trywait:嘗試等待信號量。
如果信號量值大于0,則將其減一;如果信號量值為0,則立即返回并設置錯誤碼。
int sem_trywait(sem_t *sem);
sem:指向信號量對象的指針。
返回值:成功返回0;如果信號量值為0,返回-1并設置errno為EAGAIN。
信號量實現環形隊列
這是一個用vector封裝的環形隊列,這個環形隊列是生產者和消費者的公共資源區,生產者向這個隊列里產生資源,消費者從隊列里拿走資源,但是這是一個競爭關系.
- 生產者生給隊列里產了資源,消費者才能拿
- 若隊列中資源生產滿了,那生產者就不能再生產,需要消費者消費了才行
- 若隊列中已經沒有資源了,那消費者需要等待生產者生產
RingQueue.hpp
#pragma once//環形隊列
#include<iostream>
#include<vector>
#include<semaphore.h>const int defaultsize = 5;template<class T>
class RingQueue
{
private:void P(sem_t& sem){sem_wait(&sem);}void V(sem_t &sem){sem_post(&sem);}
public:RingQueue(int size = defaultsize):_ringQueue(size),_size(size),_p_step(0),_c_step(0){sem_init(&_space,0,size); //空間資源一開始就有5個sem_init(&_data,0,0); //數據資源因為消費者還沒生產,因此還沒有}void Push(const T& in){P(_space);_ringQueue[_p_step] = in;_p_step++;_p_step %= _size;V(_data);}void Pop(T* out){P(_data);*out = _ringQueue[_c_step];_c_step++;_c_step %= _size;V(_space);}~RingQueue(){sem_destroy(&_space);sem_destroy(&_data);}private:std::vector<T> _ringQueue;int _size; //環形隊列大小int _p_step; //生產者的位置int _c_step; //消費者的sem_t _space; //生產者需要的空間sem_t _data; //消費者需要的數據
};
Main.cc
#include<iostream>
#include"RingQueue.hpp"
#include<pthread.h>
#include<unistd.h>void* productor(void* args)
{RingQueue<int>* rq = static_cast<RingQueue<int>*>(args); while(true){ int data = rand()%10;rq->Push(data);std::cout<<"i am productor :"<<data<<std::endl;}}void* consumer(void* args)
{RingQueue<int>* rq = static_cast<RingQueue<int>*>(args); while(true){sleep(1);int data = 0;rq->Pop(&data);std::cout<<"i am comsumder i get a data:"<<data<<std::endl;}}int main()
{srand((uint64_t)time(0)^pthread_self());RingQueue<int>* rq = new RingQueue<int>();pthread_t p,c;pthread_create(&p,nullptr,productor,rq);pthread_create(&c,nullptr,consumer,rq);pthread_join(p,nullptr);pthread_join(c,nullptr);delete rq;return 0;
}
Makefile
testmain:Main.ccg++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:rm -rf testmain
暫時完結