CPP中CAS std::chrono 信号量与Any类的手动实现

article/2025/8/22 18:26:12

前言

CAS(Compare and Swap) 是一种用于多线程同步的原子指令。它通过比较和交换操作来确保数据的一致性和线程安全性。CAS操作涉及三个操作数:内存位置V、预期值E和新值U。当且仅当内存位置V的值与预期值E相等时,CAS才会将内存位置V的值更新为新值U

C++中的CAS实现
在C++中,CAS操作可以通过std::atomic库中的compare_exchange_weakcompare_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;
  • 避免浅拷贝问题(派生类对象不可复制)
  • 移动操作保留以支持高效转移资源

这种模式实现了 “动态类型安全容器”:

  1. 存数据:利用模板构造函数+类型擦除 → 静态多态
  2. 取数据:通过dynamic_cast+RTTI → 动态类型检查
  3. 完美平衡了灵活性与安全性

http://www.hkcw.cn/article/DNvKWvOupp.shtml

相关文章

20250529-C#知识:索引器

C#知识&#xff1a;索引器 索引器给对象添加了索引访问的功能&#xff0c;实际访问的是对象的成员&#xff0c;感觉不太常用。 1、主要内容及代码示例 索引器中类似属性&#xff0c;也包含get和set方法索引器能够使像访问数组一样访问对象一般当类中有数组类型的成员变量时&am…

芭莎明星怼脸照,卡粉眼袋眼角纹真实却美丽,看完再也没有容貌焦虑 女星状态更胜一筹

今年的时尚芭莎盛典异常热闹。原本以为这又是一次明星们展示美貌的机会,但这次芭莎玩了个新花样,用“镜头签”将明星的真实皮肤状态暴露在公众面前。于是,“没去芭莎的很幸运了”这个词条冲上了热搜。这次的生图简直成了“照妖镜”。男星的表现可以说不尽如人意。张云龙依旧…

订单已排至2029年!我国造船产业订单量领跑全球

订单已排至2029年!我国造船产业新接订单量领跑全球在当前复杂的全球贸易形势下,我国造船产业依旧表现出强劲的市场韧性与竞争力,走出了产业加速度,今年1-4月,我国造船产业新接订单量占世界市场份额继续保持全球第一。眼下,很多造船企业的订单饱满,生产任务也排至了几年之…

Maven-生命周期

目录 1.项目对象模型 2.依赖管理模型 3.仓库&#xff1a;用于存储资源&#xff0c;管理各种jar包 4.本地仓库路径 1.项目对象模型 2.依赖管理模型 3.仓库&#xff1a;用于存储资源&#xff0c;管理各种jar包 4.本地仓库路径

Nacos

注册发现各种第三方组件的比较介绍&#xff1a; CAP C&#xff1a;一致性 A&#xff1a;可用性 P&#xff1a;分区容错性 启动nacos Linux环境&#xff1a;找到startup.sh&#xff0c;编辑文件将启动模式从集群cluster模式修改为单机模式standalone&#xff0c;如下图 然…

苹果公司计划按年份来重命名重大的软件,将升级iOS 18软件至iOS 26

苹果公司计划从今年开始&#xff0c;所有苹果操作系统将统一采用年份标识&#xff0c;而非此前混乱的版本号体系。苹果将在6月9日的全球开发者大会上正式宣布这一变革。周三截至发稿&#xff0c;苹果股价震荡微涨0.46%&#xff0c;重回3万亿美元市值。 苹果公司正在筹划其操作…

How to Initiate Back-to-Back Write Transactions from Master

Q: How to Initiate Back-to-Back Write Transactions from Master A: following are the modification required at master end to achieve back-to-back transaction driving the VIP: constraint all the master relevant delays to ‘0’ during transaction randomizatio…

纵览网丨新视角下的黑洞探索:传统奇点理论的挑战与未来观测的可能性

纵览网&#xff08;www.zonglan.com&#xff09;在宇宙的浩瀚无垠中&#xff0c;黑洞一直以其神秘莫测的特性吸引着人类的目光。从爱因斯坦的广义相对论到现代天文学的观测成果&#xff0c;黑洞的研究不断取得突破&#xff0c;但同时也伴随着无数未解之谜。其中&#xff0c;传统…

冯彬实现女子铁饼亚锦赛三连冠 中国田径再添辉煌

韩国当地时间5月29日晚,2025年亚洲田径锦标赛结束了第三个比赛日的争夺。女子铁饼决赛中,中国选手冯彬凭借最后一投的61米90顺利夺得金牌,并实现了个人的亚锦赛三连冠。这也是中国田径队连续12届摘得女子铁饼项目的亚锦赛金牌。31岁的冯彬此前曾获得2022年俄勒冈世锦赛金牌和…

4.1.4 基于数据帧做SQL查询

在本节实战中&#xff0c;我们探讨了如何基于Spark DataFrame执行SQL查询。首先&#xff0c;我们学习了如何通过createOrReplaceTempView方法将DataFrame注册为一个临时视图&#xff0c;以便在SQL查询中使用。接着&#xff0c;我们使用spark.sql方法执行了各种SQL查询&#xff…

DUBBO介绍

1.1 DUBBO简介 Dubbo是Alibaba开源的分布式服务框架&#xff0c;致力于提供高性能和透明化的RPC远程服务调用方案&#xff0c;以及SOA服务治理方案。它最大的特点是按照分层的方式来架构&#xff0c;使用这种方式可以使各个层之间解耦合&#xff08;或者最大限度地松耦合&…

这个西部城市登顶全国消费第一城 重庆连续超越上海

这个西部城市登顶全国消费第一城 重庆连续超越上海!重庆的消费数据再次超过了上海。根据重庆市统计局的数据,1-4月,重庆社会消费品零售总额达到5385.43亿元,同比增长4.4%。同期,上海的社会消费品零售总额为5355.46亿元,同比下降0.3%。这意味着重庆成为当前中国消费总额最…

solidworks报错-只有合并特征才能被阵列。如果恰当,请选择实体的阵列

当我想要阵列这个特征的时候出现了如下报错&#xff0c;报错提示我使用实体的阵列&#xff0c;但这明显不合适&#xff0c;因为我在创建特征的时候已经合并了特征所以只有一个实体&#xff0c;有一个不算聪明的解决方法 重新退回特征创建阶段&#xff0c;取消合并结果 这样设计…

【加密社】私钥碰撞工具 最新版

最近有很多朋友问我能不能做一款针对指定地址进行爆破的工具 【指定地址进行碰撞】 当然可以做。 这里要说明的是&#xff0c;私钥碰撞工具的概率是非常非常非常非常小的&#xff0c;几乎无限趋近于0的几率&#xff0c;除非你是天选之子。 &#xff08;但是这里我还是做了一…

基于React和TypeScript的金融市场模拟器开发与模式分析

基于React和TypeScript的金融市场模拟器开发与模式分析 项目概述 本项目开发了一个基于React和TypeScript的金融市场模拟器&#xff0c;通过模拟订单流和价格发现机制&#xff0c;重现了真实市场的动态特性。该模拟器不仅提供了实时价格图表、订单簿和交易功能&#xff0c;还…

进程控制与调度下

内核总控程序返回调度程序 这个点 可剥夺的调度 实现多个进程轮流运行 真正的变发运行 短进程优先问题:后面就是不断来短进程的 长进程就没法运行 优化来了:最高响应比优先法 例子:io等待太久 然后给他cpu 运行完一个时间片 然后降低优先级 给其他进程运行 Linux和window…

高精度厚金 PCB 技术白皮书:参数标准、应用案例及猎板 PCB 解决方案

一、厚金 PCB 线路板技术参数与工艺原理 厚金 PCB 通过脉冲电镀工艺在导体表面形成高纯度金层&#xff0c;核心参数需满足严苛工业标准。以猎板 PCB 的技术方案为例&#xff0c;金层厚度通常控制在 1.2-2.5μm&#xff08;典型值 1.8μm0.15μm&#xff09;&#xff0c;底层沉…

生成式人工智能重塑商业价值:从任务分解到战略跃迁的全景解析

引言 生成式人工智能&#xff08;GenAI&#xff09;正通过任务分解框架重塑商业价值&#xff0c;其核心在于精准定位“增强”与“自动化”的结合点&#xff0c;而非全盘替代人类工作。基于布林乔尔森的经济学模型&#xff0c;企业可拆解岗位任务&#xff0c;评估GenAI在效率提…

数据结构(7)树-二叉树-堆

一、树 1.树的概述 现实生活中可以说处处有树。 在计算机里&#xff0c;有一种数据结构就是像现实中的树一样&#xff0c;有根&#xff0c;有分支&#xff0c;有叶子&#xff1b;一大片树就叫做森林。 这些性质抽象到计算机里也叫树&#xff0c;大致长这个样子&#xff1a; …

MySQL入门笔记

MySQL的逻辑架构 第一层&#xff1a; 处理客户端连接、线程处理、身份验证、确保安全。每一个客户端都会在服务器进程中拥有一个线程&#xff0c;该连接的命令操作都只会在这个单独的线程执行。 第二层&#xff1a; MySQL服务器层。主要分为解析器、优化器。 查询解析、分析…