Linux 【線程池】【單例模式】【讀者寫者問題】

?💓博主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. 提高資源利用率:線程池通過預先創建一定數量的線程,避免了頻繁創建和銷毀線程的開銷,從而提高了線程資源的利用率。

  2. 降低開銷:線程的創建和銷毀都需要消耗系統資源和時間。線程池通過復用已有的線程,減少了這些開銷。

  3. 提高響應速度:線程池中的線程處于就緒狀態,可以快速響應任務請求,減少了等待線程創建的時間。

  4. 避免資源耗盡:通過限制線程池中的最大線程數,可以防止因創建過多線程而導致的系統資源耗盡。

  5. 提高線程管理效率:線程池提供了統一的線程管理機制,使得線程的調度和生命周期管理更加高效和有序。

  6. 減少上下文切換:線程池中的線程可以長時間運行,減少了線程上下文切換的頻率,從而提高了系統的整體性能。

  7. 可擴展性:線程池可以根據系統的需求和資源情況進行擴展,適應不同的工作負載。

  8. 靈活性和可配置性:線程池允許開發者根據應用程序的需要進行配置,例如設置核心線程數、最大線程數、任務隊列容量等。

  9. 易于監控和調試:線程池提供了統一的接口和機制,使得監控和調試線程相關的問題變得更加容易。

  10. 支持任務優先級:一些線程池實現支持任務優先級,允許開發者根據任務的重要性分配不同的執行優先級。

  11. 簡化編程模型:使用線程池可以簡化并發編程模型,開發者只需要關注任務的提交,而不需要關心線程的創建和管理。

  12. 支持批量任務處理:線程池可以高效地處理大量任務,特別是當任務可以并行執行時,可以顯著提高處理速度。

  13. 減少鎖競爭:通過合理設計線程池,可以減少線程之間的鎖競爭,提高并發性能。

  14. 支持復雜的并發模式:線程池可以支持多種并發模式,如工作竊取、任務調度等,以適應不同的應用場景

總之,線程池是一種有效的資源管理策略,可以提高多線程程序的性能、可伸縮性和可維護性。然而,合理地設計和使用線程池也是非常重要的,以避免潛在的問題,如死鎖、資源競爭等。?

1.3 線程池應用場景

  1. Web服務器:處理來自用戶的HTTP請求,線程池可以快速響應并發請求,提高服務器的吞吐量。

  2. 數據庫連接池:管理數據庫連接,線程池可以復用連接,減少連接創建和銷毀的開銷。

  3. 批處理系統:執行大量獨立的任務,如數據導入、導出、報告生成等,線程池可以并行處理這些任務,提高效率。

  4. 消息隊列處理:處理消息隊列中的消息,線程池可以并發地消費消息,提高消息處理速度。

  5. 網絡通信:在客戶端或服務器端進行網絡通信時,線程池可以管理多個網絡連接和數據傳輸任務。

線程池應用場景不止這些,線程池的應用場景非常廣泛,關鍵在于根據具體需求合理配置線程池的大小和特性,以達到最佳的性能和資源利用率。

下面我們就手搓線程池。?

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.單例模式

單例模式其實就是我們編寫代碼 設計模式當中的一種 .

那什么又是設計模式?

IT 行業這么火 , 涌入的人很多 . 俗話說林子大了啥鳥都有 . 大佬和菜雞們兩極分化的越來越嚴重 . 為了讓菜雞們不太拖大佬的后腿, 于是大佬們針對一些經典的常見的場景 , 給定了一些對應的解決方案 , 這個就是 設計模式

3.1 單例模式的特點

  1. 唯一性:單例模式確保一個類只有一個實例,并提供一個全局訪問點來獲取這個實例。

  2. 全局訪問點:通過一個靜態方法(例如getInstance)來獲取類的唯一實例,這個靜態方法通常被稱為“全局訪問點”。

  3. 延遲實例化:單例模式通常在第一次使用時才創建實例(懶漢式),這有助于節省資源,特別是在實例化成本較高時

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?其他常見的各種鎖

悲觀鎖:在每次取數據時,總是擔心數據會被其他線程修改,所以會在取數據前先加鎖(讀鎖,寫鎖,行鎖等),當其他線程想要訪問數據時,被阻塞掛起。
我們從線程控制到線程池用的都是悲觀鎖
樂觀鎖:每次取數據時候,總是樂觀的認為數據不會被其他線程修改,因此不上鎖。但是在更新數據前, 會判斷其他數據在更新前有沒有對數據進行修改。主要采用兩種方式:版本號機制和CAS 操作。
CAS操作:當需要更新數據時,判斷當前內存值和之前取得的值是否相等。如果相等則用新值更新。若不等則失敗,失敗則重試,一般是一個自旋的過程,即不斷重試。
自旋鎖:自旋鎖的基本思想是,當一個線程嘗試獲取一個已被其他線程持有的鎖時,它不會立即阻塞(即“睡眠”),而是在當前位置“自旋”,也就是執行一個忙等待循環,直到鎖被釋放。
初始化自旋鎖:函數原型: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):會修改數據。

問題的核心是如何設計同步機制,以滿足以下要求:

  1. 互斥:當寫者正在寫入數據時,不允許其他寫者或讀者訪問數據。
  2. 無餓死:保證所有線程最終都能訪問到數據,避免某些線程無限期地等待。
  3. 讀者優先寫者優先:根據具體場景,可以優先考慮讀者或寫者的訪問。

讀者-寫者問題有幾種不同的解決方案,包括:

使用互斥鎖的簡單解決方案:

  • 當讀者讀取數據時,他們之間不需要互斥,但寫者需要獨占訪問。
  • 這種方法簡單,但可能導致寫者饑餓,如果讀者持續不斷地訪問數據。

允許多個讀者但只有一個寫者的解決方案:

  • 使用兩個鎖,一個用于讀者之間的同步(Reader lock),另一個用于寫者(Writer lock)。
  • 讀者在進入和離開讀取狀態時分別獲取和釋放Reader lock。
  • 寫者在寫入前獲取Writer lock,并在寫入完成后釋放。

帶優先級的解決方案:

  • 讀者優先:如果有很多讀者,可以設計機制讓讀者優先獲取鎖,但這可能導致寫者饑餓。
  • 寫者優先:如果寫入操作很重要,可以設計機制讓寫者優先獲取鎖,但這可能導致讀者饑餓。

使用條件變量的解決方案:

  • 條件變量可以用于實現更靈活的等待和通知機制。
  • 讀者可以使用條件變量來等待數據未被寫者鎖定的通知。
  • 寫者可以使用條件變量來等待沒有讀者正在讀取或等待讀取的通知。

避免饑餓的解決方案:

  • 可以使用請求計數器來跟蹤讀者和寫者的請求,確保每個類型的線程都有機會訪問資源

下面使用一個互斥鎖(mutex)和兩個條件變量(cv_readercv_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, &params_readers[i]);}// 創建寫者線程for (int i = 0; i < NUM_WRITERS; ++i) {params_writers[i].id = i + NUM_READERS;pthread_create(&writers[i], nullptr, writer, &params_writers[i]);}// 等待線程結束for (int i = 0; i < NUM_READERS + NUM_WRITERS; ++i) {pthread_join(readers[i], nullptr);}return 0;
}

?

這個示例中,我們創建了多個讀者線程和一個寫者線程。讀者線程在讀取數據時不需要互斥,但寫者線程在寫入數據時需要獨占訪問。我們使用互斥鎖來保護共享資源,使用條件變量來同步讀者和寫者之間的訪問。

線程章節到此結束,從初始線程,如何控制線程,后面我們認識到了線程并發訪問臨界資源是有線程安全的問題,從而我們又學習了鎖、條件變量、信號量。通過這些我們手搓了生產消費者模型、線程池、最后認識了單例模式、還有讀者寫者問題。?

?

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

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

相關文章

ES6自定義模塊

在ES6中&#xff0c;我們可以使用 export 和 import 關鍵字來定義和使用自定義模塊。 定義模塊 導出&#xff08;export&#xff09; 命名導出&#xff08;Named Exports&#xff09;&#xff1a; 使用 export 關鍵字來導出模塊中的變量、函數、類等。例如&#xff1a; // ma…

js 復制文本帶樣式

一鍵復制帶樣式的html文本到郵件 <div><div idcopy-content><div style{{ fontSize: 16px,fontWeight: 500, lineHeight: 24px, color: #222, marginBottom: 16px }}>邀請您參加騰訊會議網絡研討會&#xff08;Webinar)</div></div><Button …

使用 /proc/sysrq-trigger 進行系統調試和故障排除

前言 在 Linux 系統中&#xff0c;/proc/sysrq-trigger 是一個強大的工具&#xff0c;提供了一種與系統進行低級別交互的方法。本文將詳細介紹 sysrq-trigger 的功能、使用方法及其在系統調試和故障排除中的應用。 什么是 /proc/sysrq-trigger? /proc/sysrq-trigger 是 Lin…

【HICE】web服務器搭建4

自定義多個ip地址訪問 1.下載httpd協議&#xff1a;dnf install httpd -y 2.編輯vhost.conf cd /etc/httpd cd /conf.d <directory /www> allowoverride none require all granted </directory> <virtualhost 192.168.244.130:80> documentroot /www s…

計算機視覺是什么,涉及的關鍵技術和應用領域

計算機視覺是一門技術&#xff0c;它是人工智能&#xff08;AI&#xff09;的一個重要分支&#xff0c;它使計算機能夠從圖像或視頻中識別、處理和理解視覺信息。它的研究和應用涉及多個領域&#xff0c;包括工業自動化、安全監控、醫療診斷、交通管理等。計算機視覺的應用非常…

07 docker 容器存儲持久化

目錄 1. Docker Volumes 特點 示例 2. Bind Mounts 特點 示例 對比總結 3. tmpfs Mounts 4. Docker Storage Plugins 5. Kubernetes Persistent Volumes 6. Network Attached Storage (NAS) 和 Storage Area Network (SAN) 1. Docker Volumes 使用存儲卷進行存儲持久…

什么是 API 代理?

API 代理就像是您的計算機和互聯網上特殊服務之間的中間人。它有點像集翻譯、保安和信使于一體。 什么是 API 代理&#xff1f; API 代理就像是您和在線服務之間的中間人。當您的計算機需要從某個特殊的在線服務 (API) 獲得某些東西時&#xff0c;API 代理會確保一切順利進行…

Eslint與Prettier搭配使用

目錄 前置準備 Eslint配置 Prettier配置 解決沖突 前置準備 首先需要安裝對應的插件 然后配置settings.json 點開之后就會進入settings.json文件里&#xff0c;加上這兩個配置 // 保存的時候自動格式化 "editor.formatOnSave": true, // 保存的時候使用prettier進…

1.2 ROS2安裝

1.2.1 安裝ROS2 整體而言&#xff0c;ROS2的安裝步驟不算復雜&#xff0c;大致步驟如下&#xff1a; 準備1&#xff1a;設置語言環境&#xff1b;準備2&#xff1a;啟動Ubuntu universe存儲庫&#xff1b;設置軟件源&#xff1b;安裝ROS2&#xff1b;配置環境。 請注意&…

拓撲學習系列(2)同調群、同倫群與基本群

同調群 同調群是拓撲空間的一個重要不變量&#xff0c;用于研究空間的“洞”的結構。同調群描述了拓撲空間中的閉合曲線、曲面等的性質&#xff0c;是拓撲學中的一個重要工具。以下是對同調群的詳細描述&#xff1a; 定義&#xff1a; 給定一個拓撲空間 X&#xff0c;對于每個…

【分布式系統】監控平臺Zabbix對接grafana

以前兩篇博客為基礎 【分布式系統】監控平臺Zabbix介紹與部署&#xff08;命令截圖版&#xff09;-CSDN博客 【分布式系統】監控平臺Zabbix自定義模版配置-CSDN博客 一.安裝grafana并啟動 添加一臺服務器192.168.80.104 初始化操作 systemctl disable --now firewalld set…

LeetCode 算法:路徑總和 III c++

原題鏈接&#x1f517;&#xff1a;路徑總和 III 難度&#xff1a;中等???? 題目 給定一個二叉樹的根節點 root &#xff0c;和一個整數 targetSum &#xff0c;求該二叉樹里節點值之和等于 targetSum 的 路徑 的數目。 路徑 不需要從根節點開始&#xff0c;也不需要在葉…

操作系統調度算法、頁面置換算法總結

常見的進程調度算法 FCFS:非搶占、先來先服務。 對短進程不利。 優先級調度算法:在支持搶占的系統中,當新進程進入就緒隊列時,如果它的優先級高于當前運行進程的優先級,那么就會搶占CPU;在非搶占系統中,只是將新進程加入了就緒隊列中。 最短作業優先調度算法(SJF) …

去中心化經濟的革新:探索Web3的新商業模式

隨著區塊鏈技術的發展&#xff0c;Web3正逐漸成為全球經濟和商業模式的關鍵詞之一。Web3不僅僅是技術的革新&#xff0c;更是對傳統中心化商業模式的挑戰和重構。本文將深入探討Web3背后的概念、關鍵技術以及其帶來的新商業模式&#xff0c;帶領讀者走進這一新興領域的深度分析…

272. 最長公共上升子序列

Powered by:NEFU AB-IN Link 文章目錄 272. 最長公共上升子序列題意思路代碼 272. 最長公共上升子序列 題意 如題 思路 若按這個思路的話&#xff0c;代碼為 O ( n 3 ) O(n^3) O(n3) for (int i 1; i < n; i ) {for (int j 1; j < n; j ){f[i][j] f[i - 1][j];…

SpringSecurity中文文檔(Servlet Password Storage)

存儲機制&#xff08;Storage Mechanisms&#xff09; 每種支持的讀取用戶名和密碼的機制都可以使用任何支持的存儲機制&#xff1a; Simple Storage with In-Memory AuthenticationRelational Databases with JDBC AuthenticationCustom data stores with UserDetailsServic…

Cube大小與性能的博弈:Kylin查詢性能優化指南

Cube大小與性能的博弈&#xff1a;Kylin查詢性能優化指南 在Apache Kylin的多維數據分析世界中&#xff0c;Cube是核心組件&#xff0c;它直接影響查詢性能和系統資源的使用。理解Cube大小與查詢性能之間的關系對于構建高效的數據分析平臺至關重要。本文將深入探討Kylin中Cube…

FW SystemUI Keyguard解析(二)

文章目錄 CTS之Keyguard Menu事件處理 CTS之Keyguard Menu事件處理 事件觸發點: NotificationShadeWindowViewController.dispatchKeyEvent 設置setInteractionEventHandler回調之后通過NotificationShadeWindowView 觸發 調用到return mService.onMenuPressed(); public cla…

31-Pandas index操作索引

Pandas index操作索引 索引&#xff08;index&#xff09;是 Pandas 的重要工具&#xff0c;通過索引可以從 DataFame 中選擇特定的行數和列數&#xff0c;這種選擇數據的方式稱為“子集選擇”。 在 Pandas 中&#xff0c;索引值也被稱為標簽&#xff08;label&#xff09;&a…

簡單的text/html無法解析解決記錄

簡單的text/html無法解析解決記錄 1. bug發現 我們所有的服務都是微服務&#xff0c;服務間調用都是使用feign接口進行調用&#xff0c;正常調用都沒有問題&#xff0c;但是某一天發現部分從esb服務調用過來到我們本地的服務&#xff0c;本地服務再使用feign接口調用其他微服…