Linux學習筆記:線程

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,這是因為以下幾步:

  1. 當前線程對自己的線程TCB中所保存票數進行判斷
  2. 將自己線程內存中的數據放到寄存器
  3. 寄存器更改
  4. 再把寄存器的數據交給線程內存進行保存

但是一頓操作下來,到其中的某一個步驟的時候,自己的線程時間片到了,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;}}}
}

線程死鎖

什么是死鎖

在多線程環境中,當兩個或多個線程相互等待對方釋放資源,從而無限期地阻塞彼此的進程,就會發生死鎖。這些資源可以是任何東西,如數據、文件或任何由互斥鎖保護的資源。

死鎖的四個條件

產生死鎖的四個必要條件:

  1. 互斥條件:資源至少有一個不能被共享,只能由一個線程占用。
  2. 持有并等待條件:一個線程至少持有一個資源,并等待獲取一個當前被其他線程持有的資源。
  3. 非搶占條件:資源不能被強制從一個線程中搶占,只能由持有資源的線程主動釋放。
  4. 循環等待條件:涉及的線程之間形成一個環路,每個線程都在等待下一個線程持有的資源。

只要沒滿足上面的四條,那就都不是死鎖,因此,想要不發生死鎖,只需要破壞上面四條中的任意一條即可

死鎖的代碼實例

下面的例子中,兩個線程嘗試獲取兩把鎖,從而導致死鎖:

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;失敗返回錯誤號。

生產者消費者模型

生產者和消費者模型是計算機領域中常用的一種資源控制方法,一般情況下,在一個完整的運行過程中,不可能只有單方面的生產者或者消費者的一方,基本都是一邊生產資源,一邊要拿資源,因此,總結下來生產者消費者模型如下:

  1. 生產者 和 消費者 間存在 競爭 和 互斥 的關系
  2. 消費者 和 消費者 間存在 競爭 和 互斥 的關系
  3. 生產 和 消費 這兩個行為之間存在 互斥 和 同步 的關系

下面是用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封裝的環形隊列,這個環形隊列是生產者和消費者的公共資源區,生產者向這個隊列里產生資源,消費者從隊列里拿走資源,但是這是一個競爭關系.

  1. 生產者生給隊列里產了資源,消費者才能拿
  2. 若隊列中資源生產滿了,那生產者就不能再生產,需要消費者消費了才行
  3. 若隊列中已經沒有資源了,那消費者需要等待生產者生產

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

暫時完結

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/15007.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/15007.shtml
英文地址,請注明出處:http://en.pswp.cn/web/15007.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

雷電預警監控系統:守護安全的重要防線

TH-LD1在自然界中&#xff0c;雷電是一種常見而強大的自然現象。它既有震撼人心的壯觀景象&#xff0c;又潛藏著巨大的安全風險。為了有效應對雷電帶來的威脅&#xff0c;雷電預警監控系統應運而生&#xff0c;成為現代社會中不可或缺的安全防護工具。 雷電預警監控系統的基本…

makefile 編寫規則

1.概念 1.1 什么是makefile Makefile 是一種文本文件&#xff0c;用于描述軟件項目的構建規則和依賴關系&#xff0c;通常用于自動化軟件構建過程。它包含了一系列規則和指令&#xff0c;告訴構建系統如何編譯和鏈接源代碼文件以生成最終的可執行文件、庫文件或者其他目標文件…

Node.js知識點以及案例總結

思考&#xff1a;為什么JavaScript可以在瀏覽器中被執行 每個瀏覽器都有JS解析引擎&#xff0c;不同的瀏覽器使用不同的JavaScript解析引擎&#xff0c;待執行的js代碼會在js解析引擎下執行 為什么JavaScript可以操作DOM和BOM 每個瀏覽器都內置了DOM、BOM這樣的API函數&#xf…

開源模型應用落地-食用指南-以最小成本博最大收獲

一、背景 時間飛逝&#xff0c;我首次撰寫的“開源大語言模型-實際應用落地”專欄已經完成了一半以上的內容。由衷感謝各位朋友的支持,希望這些內容能給正在學習的朋友們帶來一些幫助。 在這里&#xff0c;我想分享一下創作這個專欄的初心以及如何有效的&#xff0c;循序漸進的…

STM32F103C8T6 HC-SR04超聲波模塊——超聲波障礙物測距(HAl庫)

超聲波障礙物測距 一、HC-SR04超聲波模塊&#xff08;一&#xff09;什么是HC-SR04&#xff1f;&#xff08;二&#xff09;HC-SR04工作原理&#xff08;三&#xff09;如何使用HC-SR04&#xff08;四&#xff09;注意事項 二、程序編寫&#xff08;一&#xff09;CubeMX配置1.…

2024全新Langchain大模型AI應用與多智能體實戰開發

2024全新Langchain大模型AI應用與多智能體實戰開發 LangChain 就是一個 LLM 編程框架&#xff0c;你想開發一個基于 LLM 應用&#xff0c;需要什么組件它都有&#xff0c;直接使用就行&#xff1b;甚至針對常規的應用流程&#xff0c;它利用鏈(LangChain中Chain的由來)這個概念…

Facebook之魅:數字社交的體驗

在當今數字化時代&#xff0c;Facebook作為全球最大的社交平臺之一&#xff0c;承載著數十億用戶的社交需求和期待。它不僅僅是一個簡單的網站或應用程序&#xff0c;更是一個將世界各地的人們連接在一起的社交網絡&#xff0c;為用戶提供了豐富多彩、無與倫比的數字社交體驗。…

C++實現基礎二叉搜索樹(并不是AVL和紅黑樹)

本次實現的二叉搜索樹并不是AVL數和紅黑樹&#xff0c;只是了解流程和細節。 目錄 二叉搜索樹的概念K模型二叉搜索樹的實現二叉搜索樹的架構insert插入find 查找中序遍歷Inorder刪除earse替換法的思路情況一 &#xff1a;假如要刪除節點左邊是空的。在左邊時在右邊時 情況二&a…

文心智能體,零代碼構建情感表達大師智能體

前言 隨著智能體技術的突飛猛進&#xff0c;各行各業正迎來前所未有的變革與機遇。智能體&#xff0c;作為人工智能領域的重要分支&#xff0c;以其自主性、智能性和適應性&#xff0c;正逐步滲透到我們生活的每一個角落&#xff0c;成為推動社會進步和科技發展的新動力。 為了…

軟考 系統架構設計師系列知識點之雜項集萃(20)

接前一篇文章&#xff1a;軟考 系統架構設計師系列知識點之雜項集萃&#xff08;19&#xff09; 第28題 在單元測試中&#xff0c;&#xff08; &#xff09;。 A. 驅動模塊用來調用被測模塊&#xff0c;自頂向下的單元測試中不需要另外需要編寫驅動模塊 B. 樁模塊用來模擬被…

visual studio 2022 ssh 主機密鑰算法失敗問題解決

 Solution - aengusjiang 問題&#xff1a; I follow the document, then check sshd_config, uncomment“HostKey /etc/ssh/ssh_host_ecdsa_key” maybe need add the key algorithms: #HostKeyAlgorithms ssh-ed25519[Redacted][Redacted]rsa-sha2-256,rsa-sha2-512 Ho…

Redis常用命令——String篇

前面我們講解了一些 Redis 的全局命令&#xff08;Redis常用基本全局命令&#xff09;。所謂全局命令&#xff0c;就是可以匹配任意一個數據結構進行使用。但是不同的數據結構&#xff0c;也有自己的操作命令。本篇文章主要講解的是 String 的操作命令&#xff0c;希望會對你有…

ClickHouse課件

列式存儲數據庫&#xff1a;hbase clickhouse 簡介 ClickHouse入門 ClickHouse是俄羅斯的Yandex于2016年開源的列式存儲數據庫&#xff08;DBMS&#xff09;&#xff0c;使用C語言編寫&#xff0c;主要用于在線分析處理查詢&#xff08;OLAP&#xff09;&#xff0c;能夠使用…

2024年電工杯B題論文首發+問題一論文代碼分享

問題一論文代碼鏈接&#xff1a;https://pan.baidu.com/s/1kDV0DgSK3E4dv8Y6x7LExA 提取碼&#xff1a;sxjm --來自百度網盤超級會員V5的分享 基于數據分析的大學生平衡膳食食譜的優化設計及評價 摘要 大學時期不僅是學術學習和身體成長的關鍵階段&#xff0c;更是青年學生…

supermind讀寫自選股的功能來了

python custom_sector() # 返回所有板塊的dataframecustom_sector(板塊1) # 返回 板塊1 的屬性和股票custom_sector(板塊1, append, [000001.SZ]) # 增加板塊1的股票列表custom_sector(板塊1, pop, [000001.SZ]) # 移除板塊1的股票custom_sector(板塊1, remove) # 刪除板塊1zxg…

Hsql每日一題 | day03

前言 就一直向前走吧&#xff0c;沿途的花終將綻放~ 題目&#xff1a;打折日期交叉問題 如下為平臺商品促銷數據&#xff1a;字段為品牌&#xff0c;打折開始日期&#xff0c;打折結束日期 brand stt edt oppo,2021-06-05,2021-06-09 oppo,2021-06-11,2021-06-21 vivo,…

Java中流的概念細分

按流的方向分類&#xff1a; 輸入流&#xff1a;數據流向是數據源到程序&#xff08;以InputStream、Reader結尾的流&#xff09;。 輸出流&#xff1a;數據流向是程序到目的地&#xff08;以OutputStream、Writer結尾的流&#xff09;。 按處理的數據單元分類&#xff1a; 字…

PVE 虛擬機環境下刪除 local-lvm分區

1、刪除邏輯卷 lvremote pve/data 2、擴展邏輯卷 lvextend -l 100%FREE -r pve/root 3、 修改存儲目錄內容 點擊 Datacenter - Storage &#xff08;1&#xff09;刪除local-lvm分區 &#xff08;2&#xff09;編輯local分區&#xff0c;在內容一項中勾選所有可選項。

mysql 兩個不同字段的表導入數據

下面這個場景就是A表的字段和B表的字段不一樣&#xff0c;但是現在我想把b表中的數據導入到A表里面&#xff0c;下面是導入公式如下&#xff1a; 語法&#xff1a; 將SYS_ORG表中的數據導入到sys_depart&#xff0c;但是這兩個表的字段不一樣&#xff0c;在()里面填寫要新增數據…

Spring Boot 3.3 正式發布,王炸級更新,應用啟動速度直接起飛!

最新消息&#xff0c;Spring Boot 一次性發布了 3 個版本&#xff1a; 3.3.0 3.2.6 3.1.13 Spring Boot 3.3 正式發布了&#xff0c;3.1.x 在前幾天也停止維護了。 最新的支持版本如下&#xff1a; 從路線圖可以看到每個版本的終止時間&#xff0c;每個版本的生命周期只有…