《C++并发编程实战》读书笔记(1):并发、线程管控

C语言与CPP编程 2023-06-06 10:18

击上方“C语言与CPP编程”,选择“关注/置顶/星标公众号

干货福利,第一时间送达!

你好,我是飞宇。

昨天在朋友圈分享了一下自己关于《C++并发编程实战》这本书的读书笔记,收到不少点赞,今天就在公众号上分享一下自己以前的读书笔记,今天先更新第一部分,后续的读书笔记会慢慢再慢慢更新好了。

这里也顺便放一下自己的个人联系方式的二维码,听说以后公众号后不可以放二维码了我也会经常在微信上分享一些计算机学习经验以及工作体验,还有一些内推工作机会,一般不闲聊,欢迎来做点赞之交。

C++国内并发编程的书少得可怜,唯一本质量还稍微靠谱点的就这本了《C++并发编程实战》,算是矮子里了挑将军挑出来的吧,我看的是这本

《C++并发编程实战》是关于C++新标准涉及的并发与多线程功能的深度指南,从std::thread、std::mutex和std::async的基本使用方法开始,一直到复杂的内存模型和原子操作。

    第二版的英文原版与中译本都很容易购买。此外,github上也有其中文翻译https://github.com/xiaoweiChen/CPP-Concurrency-In-Action-2ed-2019以及示例代码汇总https://github.com/anthonywilliams/ccia_code_samples

第1章 你好,C++并发世界

    计算机系统中的并发包括任务切换与硬件并发,往往同时存在,关键因素是硬件支持的线程数。不论何种,本书谈论的技术都适用。

    采用并发的理由主要是分离关注点与提升性能。但并发使得代码复杂、难懂、易错,不值得时无需采用并发。

    并发的方式包括多进程与多线程。前者采用多个进程,每个进程只含一个线程,开销更大,通过昂贵的进程间通信来传递信息,但更安全并且可利用网络连接在不同计算机上并发。后者采用单一进程,内含多个线程,额外开销更低,但难以驾驭,往往暗含隐患。本书专攻多线程并发。

    并发与并行都指可调配的硬件资源同时运行多个任务,但并行更强调性能,而并发更强调分离关注点或相应能力。

    以一个简单的例子开启本书:

#include #include 
void hello() { std::cout << "Hello Concurrent World\n"; }
int main() { std::thread t(hello);    // join令主线程等待子线程 t.join();}


第2章 线程管控


2.1 线程的基本管控

    每个C++程序都含有至少一个线程,即main函数所在线程。随后,程序可通过std::thread启动更多线程;它需要头文件,可以通过任何可调用类型(函数、伪函数、lambda等)发起线程。

void do_some_work();std::thread my_thread(do_some_work);

    启动线程后需要决定是与之汇合(join)还是与之分离(detach)。如果线程销毁时还没决定,那么线程会调用std::terminate终止整个程序。只有存在关联的执行线程时,即t.joinable()返回true,才能调用join/detach。

    detach成员函数表示程序不等待线程结束,令线程在后台运行,归属权与控制权转交给C++运行时库。使用detach需确保所访问的外部数据始终正确有效,避免持有主线程的局部变量的指针/引用,否则主线程退出后该线程可能持有空悬指针/空悬引用。解决办法是将数据复制到新线程内部而非共享,或者使用join而非detach。

    join成员函数的作用是等待线程的执行结束并回收线程资源;只能调用一次,之后就不再joinable。为了防止抛出异常时跳过join,导致程序崩溃有,可以实现一个RAII类,在析构函数中保证已经汇合过。

class thread_guard {    std::thread& t_;
public: explicit thread_guard(std::thread& t) : t_(t) {} ~thread_guard() { if (t_.joinable()) { t_.join(); } } thread_guard(thread_guard const&) = delete; thread_guard& operator=(thread_guard const&) = delete;};

    


2、向线程函数传递参数

    直接向std::thread的构造函数添加更多参数即可给线程函数传递参数。不过参数是先按默认方式复制到线程内部存储空间,再被当成临时变量以右值形式传给线程函数。

    例如下面的字符串字面量hello,先以const char*形式传入,再转化为std::string类型。

void f(const std::string &);std::thread t(f,"hello");

    但如果实参是指针,那么传入指针后构造string时,指针可能已经空悬。解决办法是传参时直接转换为string。

std::thread t(f,std::string(buffer));

    如果线程函数的形参是左值引用,直接传入实参会被转化为右值再传入,导致错误。解决办法是用std::ref加以包装。

void f(int &i) { std::cout << i; }
int main() { int i = 3; std::thread t(f, std::ref(i));}

    想要使用成员函数作为线程函数的话,还需传入对象指针。例如下面的线程函数实际上调用w.f(i)。

class Widget {   public:    void f(int i) { cout << i; }};
int main() { Widget w; int i = 4; std::thread t(&Widget::f, &w, i); t.join();}

    对于只能移动不能拷贝的参数,例如unique_ptr,若实参是临时变量则自动移动,若实参是具名变量则需使用move。

void f(std::unique_ptr);
auto p = make_unique();std::thread t(f,std::move(p));



2.3 移交线程归属权

    thread掌握资源,像unique_ptr一样只能移动不能拷贝;此外当thread关联一个线程时向其移动赋值会导致程序终止。支持移动操作的容器,例如vector,可以装载std::thread对象。

    可以改进前文的thread_guard,使其支持构建并掌管线程,确保离开所在作用域前线程已完结。

class scoped_thread {    std::thread t;
public: explicit scoped_thread(std::thread t_) : t(std::move(t_)) { if (!t.joinable()) throw std::logic_error("No thread"); } ~scoped_thread() { t.join(); } scoped_thread(scoped_thread const&) = delete; scoped_thread& operator=(scoped_thread const&) = delete;};
// 使用统一初始化避免被解析为函数声明scoped_thread t{std::thread(f)}; 

2.4 在运行时选择线程数量、线程ID

    可以通过std::thread::hardware_concurrency()来获取可真正并发的线程数量,硬件信息无法获取时返回0。当用多线程分解任务时,该值是有用的指标。

    以下是并行版accumulate的简易实现,根据硬件线程数计算实际需要运算的线程数,随后将任务分解到各个线程处理,最后汇总得到结果。

// 每个线程运行的子任务template <typename Iterator, typename T>struct accumulate_block {    void operator()(Iterator first, Iterator last, T& result) {        result = std::accumulate(first, last, result);    }};
template <typename Iterator, typename T>T parallel_accumulate(Iterator first, Iterator last, T init) { unsigned long const length = std::distance(first, last); if (!length) return init;    // 每个线程至少处理25个元素 unsigned long const min_per_thread = 25; unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread; unsigned long const hardware_threads = std::thread::hardware_concurrency(); // 无法获取硬件线程数时设置为2 unsigned long const num_threads =        std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads); unsigned long const block_size = length / num_threads;        std::vector results(num_threads);    // 创建n-1个线程,因为本线程也进行运算任务 std::vector<std::thread> threads(num_threads - 1);
Iterator block_start = first; for (unsigned long i = 0; i < (num_threads - 1); ++i) { Iterator block_end = block_start; std::advance(block_end, block_size); threads[i] = std::thread(accumulate_block(), block_start, block_end, std::ref(results[i])); block_start = block_end; } accumulate_block()(block_start, last, results[num_threads - 1]);
std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));
return std::accumulate(results.begin(), results.end(), init);}

    线程ID的类型是std::thread::id,可随意复制或比较。可以通过thread的get_id()成员函数获取,也可以通过std::this_thread::get_id()获取当前线程ID。



第3章 在线程间共享数据

3.1 线程间共享数据的问题    

    并发编程中操作由多个线程负责,争先让线程执行各自的操作,结果取决于它们执行的相对顺序,这就是条件竞争。恶性条件竞争会导致未定义行为。很经典的两个线程各自递增一个全局变量十万次的例子,理想情况最后变量变为二十万,然而实际情况是这样:


3.2 用互斥保护共享数据

    可以利用名为互斥的同步原语。C++线程库保证了一旦由线程锁住某个互斥,其他线程试图加锁时必须等待,直到原先加锁的线程将其解锁。注意应以合适的粒度加锁,仅在访问共享数据期间加锁处理数据时尽可能解锁。        

    C++中通过构造std::mutex的实例来创建互斥,通过lock/unlock成员函数来加锁解锁。并不推荐直接调用成员函数,应使用其RAII类lock_guard,构造时加锁、析构时解锁。

// 使用互斥锁来保护some_liststd::list<int> some_list;std::mutex some_mutex;
void add_to_list(int new_value){ std::lock_guard<std::mutex> guard(some_mutex); some_list.push_back(new_value);}
//C++17支持类模板参数推导与scoped_lockvoid add_to_list(int new_value){ std::scoped_lock guard(some_mutex); some_list.push_back(new_value);}

    然而仍可能出现未被保护的指针/引用,或者成员函数调用了不受掌控的其他函数,因此不能向锁所在的作用域之外传递受保护数据的指针/引用。然而即使用互斥保护,有些接口仍存在固有的条件竞争。例如对于栈来说:线程1判断栈非空,随后线程2取出元素,栈空,随后线程1取出元素时出错。下面是一个解决办法的示例:

template <typename T>class threadsafe_stack {   public:    std::shared_ptr pop() {        std::lock_guard<std::mutex> lock(m);        if (data.empty()) throw empty_stack();        std::shared_ptr const res(std::make_shared(data.top()));        data.pop();        return res;    }    void pop(T& value) {        std::lock_guard<std::mutex> lock(m);        if (data.empty()) throw empty_stack();        value = data.top();        data.pop();    }    ...};

    最后,死锁是指两个线程都需要锁住两个互斥锁才能继续运行,而目前都只锁住一个,并苦苦等待对方解锁。以下是一些防范死锁的准则:1、如果已经持有锁,就不要获取第二个锁;确实需要获取多个锁时使用std::lock来一次性获取所有锁。2、一旦持锁,避免调用用户提供的程序接口避免嵌套锁。3、依从固定顺序获取锁。4、按层级加锁。5、事实上任何同步机制的循环等待都会导致死锁。

    例如swap函数需要同时获取双方的锁时:

class X {   public:    friend void swap(X& lhs, X& rhs) {        if (&lhs == &rhs) return;        std::lock(lhs.m, rhs.m);    // adopt_lock表示lhs.m已经上锁        std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);        std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);        swap(lhs.some_detail, rhs.some_detail);    }        // C++17中    friend void swap(X& lhs, X& rhs){        if (&lhs == &rhs) return;        std::scoped_lock guard(lhs.m,rhs.m);        swap(lhs.some_detail, rhs.some_detail);    }};

       unique_lock比lock_guard更灵活,不占有与之关联的互斥锁,但占用更多空间并且更慢。它提供了lock/try_lock/unlock成员函数;构造函数第二个参数传入adopt_lock表示互斥锁已上锁,传入defer_lock表示构造时无需上锁。unique_lock可移动不可复制,可以在不同作用域间转移互斥所有权,用途是让程序在同一个锁的保护下执行其他操作。


3.3 保护共享数据的其他工具

    可以通过once_flag类和call_once函数来在初始化过程中保护共享数据。

std::once_flag resource_flag;void init_resource(){ .. }
void run(){ std::call_once(resource_flag, init_resource); ...}

    C++11还规定了静态数据只会初始化一次。那么单例模板类可以这样实现:

template<class T>class Singleton {public:    static T& Instance() {        static T instance;        return instance;    }
protected: Singleton() = default; ~Singleton() = default;
private: Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;};// 使用方法:class MyClass : public Singleton {public:    ...private:    MyClass(); friend class Singleton;};

    对于读多写少的数据结构,C++14提供了shared_timed_mutex,C++17提供了功能更多的shared_mutex,那么写锁即lock_guard或unique_lock,读锁即shared_lock

    递归锁recursive_mutex允许同一线程对它多次加锁,释放所有锁后其他线程才可获取该锁。

根据公众号最新规定,文章中含有商品链接的需要标注"广告"二字

EOF

你好,我是飞宇,本硕均于某中流985 CS就读,先后于百度搜索以及字节跳动电商等部门担任Linux C/C++后端研发工程师。

同时,我也是知乎博主@韩飞宇,日常分享C/C++、计算机学习经验、工作体会,欢迎点击此处查看我以前的学习笔记&经验&分享的资源。

我组建了一些社群一起交流,群里有大牛也有小白,如果你有意可以一起进群交流。

欢迎你添加我的微信,我拉你进技术交流群。此外,我也会经常在微信上分享一些计算机学习经验以及工作体验,还有一些内推机会

加个微信,打开另一扇窗

C语言与CPP编程 C语言/C++开发,C语言/C++基础知识,C语言/C++学习路线,C语言/C++进阶,数据结构;算法;python;计算机基础等
评论
  • 电竞鼠标应用环境与客户需求电竞行业近年来发展迅速,「鼠标延迟」已成为决定游戏体验与比赛结果的关键因素。从技术角度来看,传统鼠标的延迟大约为20毫秒,入门级电竞鼠标通常为5毫秒,而高阶电竞鼠标的延迟可降低至仅2毫秒。这些差异看似微小,但在竞技激烈的游戏中,尤其在对反应和速度要求极高的场景中,每一毫秒的优化都可能带来致胜的优势。电竞比赛的普及促使玩家更加渴望降低鼠标延迟以提升竞技表现。他们希望通过精确的测试,了解不同操作系统与设定对延迟的具体影响,并寻求最佳配置方案来获得竞技优势。这样的需求推动市场
    百佳泰测试实验室 2025-01-16 15:45 197浏览
  • 80,000人到访的国际大展上,艾迈斯欧司朗有哪些亮点?感未来,光无限。近日,在慕尼黑electronica 2024现场,ams OSRAM通过多款创新DEMO展示,以及数场前瞻洞察分享,全面展示自身融合传感器、发射器及集成电路技术,精准捕捉并呈现环境信息的卓越能力。同时,ams OSRAM通过展会期间与客户、用户等行业人士,以及媒体朋友的深度交流,向业界传达其以光电技术为笔、以创新为墨,书写智能未来的深度思考。electronica 2024electronica 2024构建了一个高度国际
    艾迈斯欧司朗 2025-01-16 20:45 109浏览
  • 实用性高值得收藏!! (时源芯微)时源专注于EMC整改与服务,配备完整器件 TVS全称Transient Voltage Suppre,亦称TVS管、瞬态抑制二极管等,有单向和双向之分。单向TVS 一般应用于直流供电电路,双向TVS 应用于电压交变的电路。在直流电路的应用中,TVS被并联接入电路中。在电路处于正常运行状态时,TVS会保持截止状态,从而不对电路的正常工作产生任何影响。然而,一旦电路中出现异常的过电压,并且这个电压达到TVS的击穿阈值时,TVS的状态就会
    时源芯微 2025-01-16 14:23 130浏览
  • 一个易用且轻量化的UI可以大大提高用户的使用效率和满意度——通过快速启动、直观操作和及时反馈,帮助用户快速上手并高效完成任务;轻量化设计则可以减少资源占用,提升启动和运行速度,增强产品竞争力。LVGL(Light and Versatile Graphics Library)是一个免费开源的图形库,专为嵌入式系统设计。它以轻量级、高效和易于使用而著称,支持多种屏幕分辨率和硬件配置,并提供了丰富的GUI组件,能够帮助开发者轻松构建出美观且功能强大的用户界面。近期,飞凌嵌入式为基于NXP i.MX9
    飞凌嵌入式 2025-01-16 13:15 158浏览
  • 随着智慧科技的快速发展,智能显示器的生态圈应用变得越来越丰富多元,智能显示器不仅仅是传统的显示设备,透过结合人工智能(AI)和语音助理,它还可以成为家庭、办公室和商业环境中的核心互动接口。提供多元且个性化的服务,如智能家居控制、影音串流拨放、实时信息显示等,极大提升了使用体验。此外,智能家居系统的整合能力也不容小觑,透过智能装置之间的无缝连接,形成了强大的多元应用生态圈。企业也利用智能显示器进行会议展示和多方远程合作,大大提高效率和互动性。Smart Display Ecosystem示意图,作
    百佳泰测试实验室 2025-01-16 15:37 148浏览
  • 随着消费者对汽车驾乘体验的要求不断攀升,汽车照明系统作为确保道路安全、提升驾驶体验以及实现车辆与环境交互的重要组成,日益受到业界的高度重视。近日,2024 DVN(上海)国际汽车照明研讨会圆满落幕。作为照明与传感创新的全球领导者,艾迈斯欧司朗受邀参与主题演讲,并现场展示了其多项前沿技术。本届研讨会汇聚来自全球各地400余名汽车、照明、光源及Tier 2供应商的专业人士及专家共聚一堂。在研讨会第一环节中,艾迈斯欧司朗系统解决方案工程副总裁 Joachim Reill以深厚的专业素养,主持该环节多位
    艾迈斯欧司朗 2025-01-16 20:51 90浏览
  • 全球领先的光学解决方案供应商艾迈斯欧司朗(SIX:AMS)近日宣布,与汽车技术领先者法雷奥合作,采用创新的开放系统协议(OSP)技术,旨在改变汽车内饰照明方式,革新汽车行业座舱照明理念。结合艾迈斯欧司朗开创性的OSIRE® E3731i智能LED和法雷奥的动态环境照明系统,两家公司将为车辆内饰设计和功能设立一套全新标准。汽车内饰照明的作用日益凸显,座舱设计的主流趋势应满足终端用户的需求:即易于使用、个性化,并能提供符合用户生活方式的清晰信息。因此,动态环境照明带来了众多新机遇。智能LED的应用已
    艾迈斯欧司朗 2025-01-15 19:00 74浏览
  • 近期,智能家居领域Matter标准的制定者,全球最具影响力的科技联盟之一,连接标准联盟(Connectivity Standards Alliance,简称CSA)“利好”频出,不仅为智能家居领域的设备制造商们提供了更为快速便捷的Matter认证流程,而且苹果、三星与谷歌等智能家居平台厂商都表示会接纳CSA的Matter认证体系,并计划将其整合至各自的“Works with”项目中。那么,在本轮“利好”背景下,智能家居的设备制造商们该如何捉住机会,“掘金”万亿市场呢?重认证快通道计划,为家居设备
    华普微HOPERF 2025-01-16 10:22 157浏览
  • 日前,商务部等部门办公厅印发《手机、平板、智能手表(手环)购新补贴实施方案》明确,个人消费者购买手机、平板、智能手表(手环)3类数码产品(单件销售价格不超过6000元),可享受购新补贴。每人每类可补贴1件,每件补贴比例为减去生产、流通环节及移动运营商所有优惠后最终销售价格的15%,每件最高不超过500元。目前,京东已经做好了承接手机、平板等数码产品国补优惠的落地准备工作,未来随着各省市关于手机、平板等品类的国补开启,京东将第一时间率先上线,满足消费者的换新升级需求。为保障国补的真实有效发放,基于
    华尔街科技眼 2025-01-17 10:44 98浏览
  • 百佳泰特为您整理2025年1月各大Logo的最新规格信息,本月有更新信息的logo有HDMI、Wi-Fi、Bluetooth、DisplayHDR、ClearMR、Intel EVO。HDMI®▶ 2025年1月6日,HDMI Forum, Inc. 宣布即将发布HDMI规范2.2版本。新规范将支持更高的分辨率和刷新率,并提供更多高质量选项。更快的96Gbps 带宽可满足数据密集型沉浸式和虚拟应用对传输的要求,如 AR/VR/MR、空间现实和光场显示,以及各种商业应用,如大型数字标牌、医疗成像和
    百佳泰测试实验室 2025-01-16 15:41 142浏览
  • 晶台光耦KL817和KL3053在小家电产品(如微波炉等)辅助电源中的广泛应用。具备小功率、高性能、高度集成以及低待机功耗的特点,同时支持宽输入电压范围。▲光耦在实物应用中的产品图其一次侧集成了交流电压过零检测与信号输出功能,该功能产生的过零信号可用于精确控制继电器、可控硅等器件的过零开关动作,从而有效减小开关应力,显著提升器件的使用寿命。通过高度的集成化和先进的控制技术,该电源大幅减少了所需的外围器件数量,不仅降低了系统成本和体积,还进一步增强了整体的可靠性。▲电路示意图该电路的过零检测信号由
    晶台光耦 2025-01-16 10:12 89浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦