?💓博主CSDN主頁:麻辣韭菜💓
?
?專欄分類:Linux初窺門徑?
?
🚚代碼倉庫:Linux代碼練習🚚
?
🌹關注我🫵帶你學習更多Linux知識
? 🔝?
目錄
?🏳??🌈前言
🏳????正文
1.線程池概念
?1.1 池化技術
?1.2 線程池優點
1.3 線程池應用場景
2.線程池的實現?
第一步:創建ThreadPool類
第二步:構造、析構、創建線程
第三步:實現push、pop、HandlerTask函數
第四步:main.cc實現調用邏輯
?3.單例模式
3.1 單例模式的特點
3.2 懶漢、餓漢實現單例模式
?3.3 單例模式的應用場景
?
4.其他周邊問題
4.1 STL中的容器是否是線程安全的?
4.2?智能指針是否是線程安全的??
4.3?其他常見的各種鎖
4.4讀者、寫著問題
使用互斥鎖的簡單解決方案:
允許多個讀者但只有一個寫者的解決方案:
帶優先級的解決方案:
使用條件變量的解決方案:
避免饑餓的解決方案:
?
?🏳??🌈前言
????????線程池是一種多線程處理形式,它允許多個線程共享一個線程池中的固定數量的線程。線程池可以提高應用程序的響應速度和線程管理的效率,因為線程的創建和銷毀需要消耗資源和時間。使用線程池可以減少這些開銷。
????????線程池的使用場景非常廣泛,包括但不限于Web服務器、數據庫連接池、并發數據處理等。正確地使用線程池可以顯著提升應用程序的性能和穩定性。
🏳????正文
1.線程池概念
?1.1 池化技術
池化技術(Pooling)是一種資源管理策略,用于提高資源使用效率和性能。它通過預先分配一組資源,使得資源可以被多個客戶端或任務共享和重復使用,從而減少創建和銷毀資源的開銷。
線程池利用的就是池化技術的思想,當然不僅僅只有線程池還有很多的池也利用了池化技術
例如:數據庫連接池、內存池、對象池、HTTP連接池、文件描述符池、緩存池、套接字池等。
當然還有其他的,這里我就不一一例舉了。
?上圖像什么?不就是生產者消費模型嗎?不理解什么是生產消費者模型請先看Linux 生產消費者模型
?1.2 線程池優點
-
提高資源利用率:線程池通過預先創建一定數量的線程,避免了頻繁創建和銷毀線程的開銷,從而提高了線程資源的利用率。
-
降低開銷:線程的創建和銷毀都需要消耗系統資源和時間。線程池通過復用已有的線程,減少了這些開銷。
-
提高響應速度:線程池中的線程處于就緒狀態,可以快速響應任務請求,減少了等待線程創建的時間。
-
避免資源耗盡:通過限制線程池中的最大線程數,可以防止因創建過多線程而導致的系統資源耗盡。
-
提高線程管理效率:線程池提供了統一的線程管理機制,使得線程的調度和生命周期管理更加高效和有序。
-
減少上下文切換:線程池中的線程可以長時間運行,減少了線程上下文切換的頻率,從而提高了系統的整體性能。
-
可擴展性:線程池可以根據系統的需求和資源情況進行擴展,適應不同的工作負載。
-
靈活性和可配置性:線程池允許開發者根據應用程序的需要進行配置,例如設置核心線程數、最大線程數、任務隊列容量等。
-
易于監控和調試:線程池提供了統一的接口和機制,使得監控和調試線程相關的問題變得更加容易。
-
支持任務優先級:一些線程池實現支持任務優先級,允許開發者根據任務的重要性分配不同的執行優先級。
-
簡化編程模型:使用線程池可以簡化并發編程模型,開發者只需要關注任務的提交,而不需要關心線程的創建和管理。
-
支持批量任務處理:線程池可以高效地處理大量任務,特別是當任務可以并行執行時,可以顯著提高處理速度。
-
減少鎖競爭:通過合理設計線程池,可以減少線程之間的鎖競爭,提高并發性能。
-
支持復雜的并發模式:線程池可以支持多種并發模式,如工作竊取、任務調度等,以適應不同的應用場景
總之,線程池是一種有效的資源管理策略,可以提高多線程程序的性能、可伸縮性和可維護性。然而,合理地設計和使用線程池也是非常重要的,以避免潛在的問題,如死鎖、資源競爭等。?
1.3 線程池應用場景
-
Web服務器:處理來自用戶的HTTP請求,線程池可以快速響應并發請求,提高服務器的吞吐量。
-
數據庫連接池:管理數據庫連接,線程池可以復用連接,減少連接創建和銷毀的開銷。
-
批處理系統:執行大量獨立的任務,如數據導入、導出、報告生成等,線程池可以并行處理這些任務,提高效率。
-
消息隊列處理:處理消息隊列中的消息,線程池可以并發地消費消息,提高消息處理速度。
-
網絡通信:在客戶端或服務器端進行網絡通信時,線程池可以管理多個網絡連接和數據傳輸任務。
線程池應用場景不止這些,線程池的應用場景非常廣泛,關鍵在于根據具體需求合理配置線程池的大小和特性,以達到最佳的性能和資源利用率。
下面我們就手搓線程池。?
2.線程池的實現?
?我們要創建線程池,那是一批線程,所以我們需要一個容器來存放線程,這里我們選擇vector
既然是線程,同步和互斥那也是要安排的上。
到這里線程池這個類的類成員還需要什么??
生產出來的任務,需要一個容器來存放這些任務。這里選擇queue先進先出,總不能倒反天罡?
?老規矩我們先創建ThreadPool.hpp這個頭文件,然后創建ThreadPool這個類
?我們先創建一個線程信息的類方便我們等哈做實驗時候,打印出來更直觀。
class ThreadData
{public:pthread_t tid; //線程IDstd::string threadname; //線程名字
};
第一步:創建ThreadPool類
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <unistd.h>
#include <pthread.h>
template<class T>
class ThreadPool
{private:
std::vector<ThreadData> _threads; //線程池
std::queue<T> _tasks; //任務隊列
pthread_mutex_t _mutex; //互斥
pthread_cond_t _cond; //同步};
第二步:構造、析構、創建線程
const static int defaultnum = 5; // 默認5個線程
template <class T>
class ThreadPool
{
public:ThreadPool(int num = default): _threads(num){pthread_mutex_init(&_mutex, nullptrnum);pthread_cond_init(&_cond, nullptr);}void *HandlerTask(void *args){while (true){ sleep(1);std::cout << "新線程正在等待任務..." << std::endl;}}void start() // 創建線程{int num = _threads.size();for (int i = 0; i < num; i++){_threads[i].threadname = "thread-" + std::to_string(i + 1);pthread_create(&(_threads[i].tid), nullptr, HandlerTask, nullptr);}}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}private:std::vector<ThreadData> _threads; // 線程池std::queue<T> _tasks; // 任務隊列pthread_mutex_t _mutex; // 互斥pthread_cond_t _cond; // 同步
};
?我們在上層調用鏈調用,試試我們的框架能不能跑起來。
編寫mian.cc調用邏輯?
#include "ThreadPool.hpp"int main()
{ThreadPool<int> *tp = new ThreadPool<int>(5);tp->start();while (true){sleep(1);std::cout << "主線線程正在運行..." << std::endl;}return 0;
}
編譯出錯了,原因是因為我們的HandlerTask函數是在ThreadPool類中實現的,就是類內函數,而內類函數自帶this指針。而pthread_create函數要求的是void* 所以類型不匹配。所以我們需要在HandlerTask 函數前面加static,變成靜態成員函數。
static void* HandlerTask(void* args)
框架沒有問題,下面實現push和pop
第三步:實現push、pop、HandlerTask函數
?push和pop、HandlerTask涉及對臨界資源讀寫,需要對線程同步和互斥,為了方便我們先對鎖和條件變量的操作進行封裝。
public:// 加鎖void Lock(){pthread_mutex_lock(&_mutex);}// 解鎖void UnLock(){pthread_mutex_unlock(&_mutex);}// 條件滿足喚醒線程void WakeUp(){pthread_cond_signal(&_cond);}// 條件不滿足線程休眠void ThreadSleep(){pthread_cond_wait(&_cond);}// 判斷任務隊列空滿bool IsQueueEmpty(){return _tasks.empty();}
實現push
void push(const T &in){Lock();_tasks.push(in);WakeUp();UnLock();}
這里再push數據完成之后,我們需要喚醒其他線程。來進行任務處理。所以push后面跟著wakeup
實現?HandlerTask
?由于回調函數是靜態函數,所以我們調不動類內的非靜態的函數以及非靜態成員。所以我們要想調用這些函數,只有通過實例調用也是指針或者引用。這里我們直接在pthread_create這里回調參數傳this指針
pthread_create(&(_threads[i].tid), nullptr, HandlerTask, this);
static void *HandlerTask(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);while (true){tp->Lock();while (IsQueueEmpty()){tp->ThreadSleep();}T t = tp->pop();tp->UnLock();t();std::cout << "新線程正在處理任務..." << endl;}}
?注意:這里用while判斷防止偽喚醒。當隊列不為空時,線程拿到任務屬于線程自己的,所以不用在鎖里處理任務,從而提高并發度。
?實現pop
T pop(){T out = _tasks.front();_tasks.pop();return out;}
?pop調用邏輯是在回調函數中的,而回調函數是有加鎖的,所以pop這里就不用加鎖了。
第四步:main.cc實現調用邏輯
實現之前我們直接把Task.hpp這個頭文件包含進來。
?這里我們直接用把上篇的Task.hpp拷貝過來。
頭文件Task.hpp
#pragma once
#include <iostream>
#include <string>std::string opers = "+-*/%";enum
{DivZero = 1,ModZero,Unknown
};class Task
{
public:Task(){};Task(int data1, int data2, char oper): _data1(data1), _data2(data2), _oper(oper), _result(0), _exitcode(0){}void run(){switch (_oper){case '+':_result = _data1 + _data2;break;case '-':_result = _data1 - _data2;break;case '*':_result = _data1 * _data2;break;case '/':{if (_data2 == 0)_exitcode = DivZero;else_result = _data1 / _data2;}break;case '%':{if (_data2 == 0)_exitcode = ModZero;else_result = _data1 % _data2;}break;default:_exitcode = Unknown;break;}}std::string GetResult(){std::string r = std::to_string(_data1);r += _oper;r += std::to_string(_data2);r += "=";r += std::to_string(_result);r += "[code: ";r += std::to_string(_exitcode);r += "]";return r;}std::string GetTask(){std::string r = std::to_string(_data1);r += _oper;r += std::to_string(_data2);r += "=?";return r;}void operator()(){run();}~Task(){}private:int _data1;int _data2;char _oper;int _result;int _exitcode;
};
實現調用線程池?
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <ctime>
#include <mutex>
std:: mutex mtx;
int main()
{srand(time(nullptr) ^ getpid());ThreadPool<Task> *tp = new ThreadPool<Task>(5);tp->start();while (true){int x = rand() % 10 + 1;int y = rand() % 10;char oper = opers[rand() % opers.size()];Task t(x, y, oper);tp->push(t);std::cout << "主線程發布任務..." << t.GetTask() << std::endl;sleep(1);}return 0;
}
?這里不知道是那個線程在處理任務。我們重新改造
在頭文件ThreadPool增加這段函數?
std:: string GetThreadName(pthread_t tid){for(const auto& ti:_threads){if(ti.tid == tid){return ti.threadname;}}return "None";}
static void *HandlerTask(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);std:: string name = tp->GetThreadName(pthread_self());while (true){tp->Lock();while (tp->IsQueueEmpty()){tp->ThreadSleep();}T t = tp->pop();tp->UnLock();t();std::cout << name << "正在處理任務..." <<"運行結果是:" << t.GetResult()<< std::endl;}}
這樣就優美很多了,更直觀了?,這里打印錯亂很正常,cout本質就是訪問臨界資源顯示器,這里我們不用管。
?3.單例模式
那什么又是設計模式?
3.1 單例模式的特點
-
唯一性:單例模式確保一個類只有一個實例,并提供一個全局訪問點來獲取這個實例。
-
全局訪問點:通過一個靜態方法(例如
getInstance
)來獲取類的唯一實例,這個靜態方法通常被稱為“全局訪問點”。 -
延遲實例化:單例模式通常在第一次使用時才創建實例(懶漢式),這有助于節省資源,特別是在實例化成本較高時
3.2 懶漢、餓漢實現單例模式
如何創建一個單例模式?只要外部無法訪問類的構造函數,也就是將構造函數私有化,同時刪除拷貝構造。
class Singleton {// 私有化構造函數和拷貝賦值操作符
private:Singleton() {}Singleton(const Singleton&) = delete;
};
?外部無法訪問,那我們只有類內創建對象。
class Singleton {
public:Singleton* getSingleton(){if (_stl = nullptr){_stl = new Singleton;}return _stl;}// 私有化構造函數和拷貝賦值操作符
private:Singleton() {}Singleton(const Singleton&) = delete;
private:Singleton* _stl = nullptr;
};
?這樣創建有問題嗎?
由于我們是非靜態成員,這就導致了?訪問限制;
非靜態成員調用時,是需要實例化的,也就是說,我們調用getSingleton這個非靜態成員函數,是需要用對象來調。我們要創建一個對象。這就違反了單例模式的原則。
所以我們需要靜態的成員和靜態的成員函數。
class Singleton {
public:static Singleton* getSingleton(){if (_stl = nullptr){_stl = new Singleton;}return _stl;}// 私有化構造函數和拷貝賦值操作符
private:Singleton() {}Singleton(const Singleton&) = delete;
private:static Singleton* _stl; //在內類聲明
};
// 在類外定義靜態成員變量
Singleton* Singleton::_stl = nullptr;
// 定義靜態成員變量int main()
{Singleton* slt = Singleton::getSingleton();return 0;
}?
上面這種方式就是懶漢式的單例模式,
懶漢式:??
- 按需實例化:單例對象在第一次被使用時才創建,因此稱為“懶漢式”。
- 線程不安全:由于實例化發生在運行時,如果多個線程同時訪問單例對象,可能會創建多個實例,需要額外的同步機制來保證線程安全。
- 節省資源:只有在真正需要使用單例對象時才會創建,節省了資源。
下面我們用餓漢式來創建單例模式?
class Singleton {
private:static Singleton instance;public:static Singleton& getInstance() {return instance;}// 私有化構造函數和拷貝賦值操作符
private:Singleton() {}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;
};// 定義靜態成員變量
Singleton Singleton::instance;
餓漢式:
- 類加載時實例化:當類被加載時,單例對象就被創建了,因此稱為“餓漢式”。
- 線程安全:由于實例化發生在類加載階段,這個階段是線程安全的,因此餓漢式單例是線程安全的。
- 資源消耗:由于實例化時機早,即使后續沒有使用到單例對象,它也會被創建,這可能導致資源的浪費。
?3.3 單例模式的應用場景
前面我們手的搓的線程池就是單例模式的應用場景,我們調用start函數,至始至終都只實例化了一份對象。
加之線程池的生命周期是要跨越整個進程的生命周期,恰好單例模式的生命周期也是一樣。
線程池是要全局的,一個應用程序要隨時隨地都能調用到線程池,單例模式就是全局的。
居于這樣,把之前的線程池變成懶漢模式?
先將構造函數私有化、同時禁掉拷貝構造和賦值重載?
private:ThreadPool(int num = defaultnum): _threads(num){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}ThreadPool(const ThreadPool<T> &) = delete;const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
聲明靜態成員指針?
static ThreadPool<T> *_tp; // 單例模式
?類外初始化
template <class T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;
實現GetInstance()函數?
static ThreadPool<T> *GetInstance(){if (_tp == nullptr){_tp = new ThreadPool<T>;}return _tp;}
這樣寫有沒有沒有問題?當然有問題啊,多個線程同時訪問判斷_tp是不是空。這不亂套了?要加鎖!!!
由于都是靜態的,是全局的,所以我們的鎖也要是全局的。?
類成員添加下面這段代碼?
static pthread_mutex_t _lock; //保證_tp線程安全
類外初始化?
template <class T>
pthread_mutex_t ThreadPool<T>::_lock = PTHREAD_MUTEX_INITIALIZER;
下面我們加鎖?
static ThreadPool<T> *GetInstance(){pthread_mutex_lock(&_lock);if (_tp == nullptr){_tp = new ThreadPool<T>;}pthread_mutex_unlock(&_lock);return _tp;}
?加鎖之后還有沒有問題?我們是單例模式,也就是說這里只會對_tp進行一次實例化。
后面其他線程其實壓根不用加鎖解鎖操作。加鎖解鎖操作多了極大降低效率。所以我們需要進行二次判斷
static ThreadPool<T> *GetInstance(){if (_tp == nullptr){pthread_mutex_lock(&_lock);if (_tp == nullptr){_tp = new ThreadPool<T>;}pthread_mutex_unlock(&_lock);}return _tp;}
二次判斷后,如果是已經實例化,那么后面的線程再調用就是直接返回_tp。
?那我們之前在main.cc調用邏輯也要變了之前是非靜態的。現在是靜態的
int main()
{srand(time(nullptr) ^ getpid());ThreadPool<Task>::GetInstance()->start(); //之前是tpwhile (true){int x = rand() % 10 + 1;int y = rand() % 10;char oper = opers[rand() % opers.size()];Task t(x, y, oper);ThreadPool<Task>::GetInstance()->push(t);std::cout << "主線程發布任務..." << t.GetTask() << std::endl;sleep(1);}return 0;
}
?
運行沒有問題 這里要是有強迫癥的鐵子,可以在打印加鎖。
?
4.其他周邊問題
4.1 STL中的容器是否是線程安全的?
原因是, STL 的設計初衷是將性能挖掘到極致, 而一旦涉及到加鎖保證線程安全, 會對性能造成巨大的影響.而且對于不同的容器, 加鎖方式的不同, 性能可能也不同(例如hash表的鎖表和鎖桶).因此 STL 默認不是線程安全. 如果需要在多線程環境下使用, 往往需要調用者自行保證線程安全
4.2?智能指針是否是線程安全的??
對于 unique_ptr, 由于只是在當前代碼塊范圍內生效 , 因此不涉及線程安全問題 .對于 shared_ptr, 多個對象需要共用一個引用計數變量 , 所以會存在線程安全問題 . 但是標準庫實現的時候考慮到了這個問題, 基于原子操作 (CAS) 的方式保證 shared_ptr 能夠高效 , 原子的操作引用計數 .對于 weak_ptr ,??是針對share_ptr 循環引用問題而誕生的,它持有一個對象的弱引用,不增加對象的引用計數它不支持原子操作。詳情請看 C++ 智能指針_c++智能指針
4.3?其他常見的各種鎖
我們從線程控制到線程池用的都是悲觀鎖
初始化自旋鎖:函數原型:int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
說明:初始化一個自旋鎖。pshared 參數定義了自旋鎖的作用域,可以是 PTHREAD_PROCESS_PRIVATE(僅在創建它的進程中有效)或 PTHREAD_PROCESS_SHARED(可以在多個進程間共享)。
鎖定自旋鎖:函數原型:int pthread_spin_lock(pthread_spinlock_t *lock);
說明:嘗試獲取(鎖定)指定的自旋鎖。如果自旋鎖當前未被其他線程持有,則調用線程立即獲得該自旋鎖;如果已被持有,則調用線程將不斷循環檢查直到自旋鎖被釋放。
嘗試鎖定自旋鎖(非阻塞):函數原型:int pthread_spin_trylock(pthread_spinlock_t *lock);
說明:嘗試獲取自旋鎖,如果自旋鎖已被其他線程持有,則立即返回錯誤 EBUSY,而不是持續占用 CPU 資源進行自旋。
解鎖自旋鎖:函數原型:int pthread_spin_unlock(pthread_spinlock_t *lock);
說明:釋放指定的自旋鎖,如果有其他線程正在自旋等待此鎖,其中一個線程將獲得該鎖。
銷毀自旋鎖:函數原型:int pthread_spin_destroy(pthread_spinlock_t *lock);
說明:銷毀一個已經初始化的自旋鎖,釋放與鎖相關的資源。銷毀自旋鎖后,不應再使用該鎖,除非重新初始化。
非公平鎖的實現就是基于上面這些鎖加條件變量?
4.4讀者、寫著問題
是一個經典的計算機科學問題,涉及到多個線程對共享數據的訪問控制。
有多個線程需要訪問同一資源(比如文件或數據庫),這些線程被分為兩類:
- 讀者(Readers):只讀取數據,不修改數據。
- 寫者(Writers):會修改數據。
問題的核心是如何設計同步機制,以滿足以下要求:
- 互斥:當寫者正在寫入數據時,不允許其他寫者或讀者訪問數據。
- 無餓死:保證所有線程最終都能訪問到數據,避免某些線程無限期地等待。
- 讀者優先或寫者優先:根據具體場景,可以優先考慮讀者或寫者的訪問。
讀者-寫者問題有幾種不同的解決方案,包括:
使用互斥鎖的簡單解決方案:
- 當讀者讀取數據時,他們之間不需要互斥,但寫者需要獨占訪問。
- 這種方法簡單,但可能導致寫者饑餓,如果讀者持續不斷地訪問數據。
允許多個讀者但只有一個寫者的解決方案:
- 使用兩個鎖,一個用于讀者之間的同步(Reader lock),另一個用于寫者(Writer lock)。
- 讀者在進入和離開讀取狀態時分別獲取和釋放Reader lock。
- 寫者在寫入前獲取Writer lock,并在寫入完成后釋放。
帶優先級的解決方案:
- 讀者優先:如果有很多讀者,可以設計機制讓讀者優先獲取鎖,但這可能導致寫者饑餓。
- 寫者優先:如果寫入操作很重要,可以設計機制讓寫者優先獲取鎖,但這可能導致讀者饑餓。
使用條件變量的解決方案:
- 條件變量可以用于實現更靈活的等待和通知機制。
- 讀者可以使用條件變量來等待數據未被寫者鎖定的通知。
- 寫者可以使用條件變量來等待沒有讀者正在讀取或等待讀取的通知。
避免饑餓的解決方案:
- 可以使用請求計數器來跟蹤讀者和寫者的請求,確保每個類型的線程都有機會訪問資源
下面使用一個互斥鎖(mutex
)和兩個條件變量(cv_reader
和 cv_writer
)來分別同步讀者和寫者。?
#include <iostream>
#include <pthread.h>
#include <unistd.h>const int MAX_READERS = 5;
const int NUM_READERS = 3;
const int NUM_WRITERS = 2;// 共享資源
int shared_data = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t no_readers = PTHREAD_COND_INITIALIZER;
pthread_cond_t no_writers = PTHREAD_COND_INITIALIZER;// 線程參數結構體
struct ThreadParam {int id;
};// 讀者線程函數
void* reader(void* arg) {ThreadParam* param = static_cast<ThreadParam*>(arg);int reader_id = param->id;while (true) {pthread_mutex_lock(&mutex);while (shared_data < 0) {pthread_cond_wait(&no_writers, &mutex);}pthread_mutex_unlock(&mutex);std::cout << "Reader " << reader_id << " reads: " << shared_data << std::endl;sleep(1); // 模擬讀取時間// 讀者讀取完畢,喚醒寫者pthread_mutex_lock(&mutex);pthread_cond_signal(&no_writers);pthread_mutex_unlock(&mutex);}return nullptr;
}// 寫者線程函數
void* writer(void* arg) {ThreadParam* param = static_cast<ThreadParam*>(arg);int writer_id = param->id;while (true) {pthread_mutex_lock(&mutex);while (shared_data >= 0) {pthread_cond_wait(&no_readers, &mutex);}shared_data--; // 寫者進入pthread_mutex_unlock(&mutex);std::cout << "Writer " << writer_id << " writes: " << -shared_data << std::endl;sleep(2); // 模擬寫入時間pthread_mutex_lock(&mutex);shared_data++; // 寫者退出pthread_cond_broadcast(&no_writers); // 喚醒所有讀者pthread_mutex_unlock(&mutex);}return nullptr;
}int main() {pthread_t readers[NUM_READERS], writers[NUM_WRITERS];ThreadParam params_readers[NUM_READERS], params_writers[NUM_WRITERS];// 創建讀者線程for (int i = 0; i < NUM_READERS; ++i) {params_readers[i].id = i;pthread_create(&readers[i], nullptr, reader, ¶ms_readers[i]);}// 創建寫者線程for (int i = 0; i < NUM_WRITERS; ++i) {params_writers[i].id = i + NUM_READERS;pthread_create(&writers[i], nullptr, writer, ¶ms_writers[i]);}// 等待線程結束for (int i = 0; i < NUM_READERS + NUM_WRITERS; ++i) {pthread_join(readers[i], nullptr);}return 0;
}
?
這個示例中,我們創建了多個讀者線程和一個寫者線程。讀者線程在讀取數據時不需要互斥,但寫者線程在寫入數據時需要獨占訪問。我們使用互斥鎖來保護共享資源,使用條件變量來同步讀者和寫者之間的訪問。
線程章節到此結束,從初始線程,如何控制線程,后面我們認識到了線程并發訪問臨界資源是有線程安全的問題,從而我們又學習了鎖、條件變量、信號量。通過這些我們手搓了生產消費者模型、線程池、最后認識了單例模式、還有讀者寫者問題。?
?