前言
CAS(Compare and Swap) 是一種用于多線程同步的原子指令。它通過比較和交換操作來確保數據的一致性和線程安全性。CAS操作涉及三個操作數:內存位置V、預期值E和新值U。當且僅當內存位置V的值與預期值E相等時,CAS才會將內存位置V的值更新為新值U
C++中的CAS實現
在C++中,CAS操作可以通過std::atomic
庫中的compare_exchange_weak
和compare_exchange_strong
方法實現。這兩個方法都用于比較和交換原子對象的值,但它們在失敗時的行為有所不同
順帶提一下標準庫實現的延時操作std::chrono
1.原子操作
我們平時直接進行的數據修改一般都是非原子操作,如果多個線程同時以非原子操作的方式修改同一個對象可能會發生數據爭用,從而導致未定義行為;而原子操作能夠保證多個線程順序訪問,不會導致數據爭用,其執行時沒有任何其它線程能夠修改相同的原子對象。C++中可以使用std::atomic
來定義原子變量。
CAS
常見計數器用法:
std::atomic<int> counter(0);
// 線程1增加計數器
counter.fetch_add(1);
// 線程2減少計數器
counter.fetch_sub(1);
常見控制標志用法:
std::atomic<bool> flag(true);
// 線程1檢查標志
if (flag.load()) {// 執行操作
}
// 線程2修改標志
flag.store(false);
復雜數據類型用法:
#include <atomic>
#include <iostream>
#include <type_traits>
// 自定義類型 Point
struct Point {int x;int y;// 默認構造函數Point() : x(0), y(0) {}// 自定義構造函數Point(int x, int y) : x(x), y(y) {}// 拷貝構造函數和拷貝賦值運算符Point(const Point&) = default;Point& operator=(const Point&) = default;// 析構函數~Point() = default;
};
int main() {static_assert(std::is_trivially_copyable<Point>::value, "Point must be trivially copyable");std::atomic<Point> atomic_point;Point p1(1, 2);atomic_point.store(p1);Point p2 = atomic_point.load();std::cout << "Atomic Point: (" << p2.x << ", " << p2.y << ")" << std::endl;return 0;
}
2. std::chrono
std::chrono
是C++11引入的一個全新的有關時間處理的庫。
新標準以前的C++往往會使用定義在ctime頭文件中的C-Style時間庫std::time。
相較于舊的庫,std::chrono
完善地定義了時間段(duration)、時鐘(clock)和時間點(time point)三個概念,并且給出了對多種時間單位的支持,提供了更高的計時精度、更友好的單位處理以及更方便的算術操作(以及更好的類型安全)。
下面,我們將逐步說明std::chrono
用法。
chrono庫概念與相關用法
時間段(duration)
時間段被定義為std::chrono::duration
,表示一段時間。
它的簽名如下:
template<class Rep,class Period = std::ratio<1>
> class duration;
Rep是一個算術類型,表示tick數的類型,筆者一般會將其定義為int或者long long等整數類型,當然浮點數類型也是可行的。
Period代表tick的計數周期,它具有一個默認值——以一秒為周期,即 1 tick/s 。單位需要自行指定的情況會在后面涉及,這里暫時不討論。
簡單來說,我們可以認為一個未指定Period的duration
是一個以秒為單位的時間段。
一個簡單的例子:
#include <chrono>
#include <thread>
#include <iostream>
int main()
{std::chrono::duration<int> dur(2);std::cout << std::chrono::time_point_cast<std::chrono::seconds>(std::chrono::steady_clock::now()).time_since_epoch().count() << std::endl; // 以秒為單位輸出當前時間std::this_thread::sleep_for(dur);std::cout << std::chrono::time_point_cast<std::chrono::seconds>(std::chrono::steady_clock::now()).time_since_epoch().count() << std::endl; // 以秒為單位輸出當前時間return 0;
}
這段代碼的作用是輸出當前時間,隨后睡眠兩秒,再輸出當前時間。dur描述了一個2秒的時間間隔。
duration
支持幾乎所有的算術運算。通俗地說,你可以對兩個duration
做加減運算,也可以對某個duration
做數乘運算。
當然他也可以直接用于線程延時中
如下:
std::this_thread::sleep_for(std::chrono::seconds(2));
3.信號量
信號量的核心概念
頭文件在C++20中是并發庫技術規范(Technical Specification, TS)的一部分。信號量是同步原語,幫助控制多線程程序中對共享資源的訪問。頭文件提供了標準C++方式來使用信號量。
作用:
- 通過計數器限制對共享資源的并發訪問數量。
- 實現線程間的同步(如生產者-消費者模型)。
類型:
- 計數信號量(std::counting_semaphore):允許指定資源的最大并發數。
- 二元信號量(std::binary_semaphore):計數為 1 的特殊信號量(類似于互斥鎖)。
std提供的信號量如下:
#include <semaphore.h>// 用于讀寫線程之間的通信
sem_t rwsem;// 初始化讀寫線程通信用的信號量
sem_init(&rwsem, 0, 0);
sem_wait(&rwsem); // 等待信號量,子線程處理完注冊消息會通知
sem_destroy(&rwsem);
在非c++20的情況下使用信號量需要自己實現,實現如下:
信號量的簡單實現與使用
Semaphore.h文件
//實現一個信號量類
class Semaphore
{
public:Semaphore(int limit = 0):resLimit_(limit){}~Semaphore() = default;//獲取一個信號量資源void wait(){std::unique_lock<std::mutex> lock(mtx_);//等待信號量有資源,沒有資源的話,會阻塞當前線程cond_.wait(lock, [&]()->bool {return resLimit_ > 0; });resLimit_--;}//增加一個信號量資源void post(){std::unique_lock<std::mutex> lock(mtx_);resLimit_++;cond_.notify_all();}
private:int resLimit_;std::mutex mtx_;std::condition_variable cond_;
};
顯然上述cond_.wait(lock, [&]()->bool {return resLimit_ > 0; });
處的條件決定了是計數信號量還是二元信號量
Result.h文件
//實現接收提交到線程池的task任務執行完成后的返回值類型Result
class Result {
public:Result(std::shared_ptr<Task> task, bool isValid = true);~Result() = default;//問題一:setva1方法,獲取任務執行完的返回值的void setVal(Any any);//問題二:get方法,用戶調用這個方法獲取task的返回值Any get();
private:Any any_;//存儲任務的返回值Semaphore sem_;//線程通信信號量std::shared_ptr<Task> task_;//指向對應獲取返回值的任務對象std::atomic_bool isValid_;//返回值是否有效};
Result.cpp文件
//Result方法的實現
Result::Result(std::shared_ptr<Task> task, bool isValid):isValid_(isValid),task_(task)
{task_->setResult(this);
}Any Result::get()//用戶調用
{if (!isValid_){return "";}sem_.wait(); //task任務如果沒有執行完,這里會阻塞用戶的線程return std::move(any_);
}void Result::setVal(Any any)//誰調用呢
{//存儲task的返回值this->any_ = std::move(any);sem_.post();//已經獲取的任務的返回值,增加信號量資源
}
4. Any類
C++17的三劍客分別是std::optional
, std::any
, std::vairant
4.1 Any類介紹
在日常編程中,我們可能會遇到這么一個場景:需要一個類型可以接收任意類型的變量,并且在需要使用該變量的時候還能恰當的進行轉換。不難想到,C語言中的萬能指針void可以滿足我們上述的需求。但void的使用相對繁瑣,且難免會涉及到大量的內存管理操作,這無疑加大了我們編程的復雜度。而在C++17中,any類的出現很好的解決了我們上述的問題。
std::any 是 C++17 引入的一個標準庫類型,用于表示一個可以存儲任意類型數據的容器。與 std::variant 不同,std::any 不限制存儲的類型,因此它可以用來存儲任意的對象。它的設計目標是提供一種簡單的方式來存儲和檢索任意類型的值,而不需要像 void* 那樣手動管理類型信息。
std::any 的基本特性
任意類型的存儲:std::any 可以存儲任何可拷貝構造的類型。
類型安全:std::any 提供了類型安全的訪問,確保在訪問值時不會發生類型錯誤。
動態類型:std::any 可以在運行時存儲不同類型的對象,而無需在編譯時指定類型。
下面是手動實現的簡陋版Any類
//Any類型:可以接收任意數據的類型
class Any
{
public:Any() = default; ~Any() = default; Any(const Any&) = delete; Any& operator=(const Any&) = delete; Any(Any&&) = default; Any& operator=(Any&&) = default;template<typename T>Any(T data) :base_(std::make_unique<Derive<T>>(data)){}//這個方法能把any對象中存的數據提取出來template<typename T> //T:int Derive<int>T cast_(){//我們怎么從base_中找到它所指向的Derive對象,從他里面取出data對象//基類指針=》派生類指針 RTTIDerive<T>* pd = dynamic_cast<Derive<T>>(base_.get(); //使用智能指針的get方法獲取裸指針if (pd == nullptr){throw "type is unmatch!";}return pd->data_;}
private://基類類型class Base{public:virtual ~Base() = default;};//派生類類型template<typename T>class Derive :public Base{public:Derive(T data) : data_(data){}T data_; //保存了任意的其他類型};private://定義一個基類的指針std::unique_ptr<Base> base_;
};
4.2 Any類實現細節分析
4.2.1 基類取用派生類成員
首先明確一點,在C++中,基類指針不能直接訪問其所指向派生類的特有成員,這是面向對象編程中類型安全的重要規則。
所以在要取用所存儲數據時需要對base_
指針進行向下轉型
Derive<T>* pd = dynamic_cast<Derive<T>>(base_.get();
當然也可以使用另一種方法,即借用虛函數
class Base {
public:virtual void execute() = 0; // 純虛函數接口
};class Derived : public Base {
public:void execute() override { special(); // 通過多態間接調用}void special() {} // 派生類實現
};Base* ptr = new Derived();
ptr->execute(); // 實際調用Derived::execute()
4.2.2 隱式模板構造函數
使用隱式模板構造函數來免去指明數據類型
template<typename T>
Any(T data) : base_(std::make_unique<Derive<T>>(data)) {}
構造函數是模板函數,能根據傳入的data自動推導類型T
例如 Any a(10); 編譯器自動推導 T = int
4.2.3 類型擦除設計
類型擦除(Type Erasure)是一種設計模式,用來隱藏對象的具體類型,統一暴露抽象接口,提供“運行時多態”。
通過基類指針 unique_ptr 指向模板派生類 Derive
基類 Base 不含類型信息,實現類型擦除
4.2.4 派生類模板封裝
template<typename T>
class Derive : public Base {T data_; // 實際存儲的數據
};
每個不同類型的數據都會被封裝到獨立的 Derive<T>
中
用戶無需感知具體存儲類型
4.2.5 提取數據時需要指定類型的原因
//這個方法能把any對象中存的數據提取出來template<typename T> //T:int Derive<int>T cast_(){//我們怎么從base_中找到它所指向的Derive對象,從他里面取出data對象//基類指針=》派生類指針 RTTIDerive<T>* pd = dynamic_cast<Derive<T>>(base_.get(); //使用智能指針的get方法獲取裸指針if (pd == nullptr){throw "type is unmatch!";}return pd->data_;}
Any test(10);test.cast_<int>();
類型安全恢復
- 必須通過
dynamic_cast
嘗試將基類指針轉為具體的Derive<T>*
- 需要明確的模板參數
T
來恢復原始類型
運行時類型檢查
- 如果實際存儲類型與請求類型不匹配:
Any a(std::string("test")); a.cast_<int>(); // 拋出異常
dynamic_cast
失敗返回nullptr
觸發異常
關鍵技術亮點
RAII資源管理
-
使用
unique_ptr
自動管理派生類對象生命周期 -
默認移動操作支持容器存儲:
std::vector<Any> vec; vec.push_back(Any(42)); // 存int vec.push_back(Any("hello")); // 存const char*
類型安全邊界
- 構造時隱式類型推導(安全)
- 提取時顯式類型聲明(安全)
- 運行時驗證類型匹配(安全)
禁止拷貝的合理性
Any(const Any&) = delete;
- 避免淺拷貝問題(派生類對象不可復制)
- 移動操作保留以支持高效轉移資源
這種模式實現了 “動態類型安全容器”:
- 存數據:利用模板構造函數+類型擦除 → 靜態多態
- 取數據:通過dynamic_cast+RTTI → 動態類型檢查
- 完美平衡了靈活性與安全性