《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;计算机基础等
评论
  • 习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-11 17:58 88浏览
  • 在智能化技术快速发展当下,图像数据的采集与处理逐渐成为自动驾驶、工业等领域的一项关键技术。高质量的图像数据采集与算法集成测试都是确保系统性能和可靠性的关键。随着技术的不断进步,对于图像数据的采集、处理和分析的需求日益增长,这不仅要求我们拥有高性能的相机硬件,还要求我们能够高效地集成和测试各种算法。我们探索了一种多源相机数据采集与算法集成测试方案,能够满足不同应用场景下对图像采集和算法测试的多样化需求,确保数据的准确性和算法的有效性。一、相机组成相机一般由镜头(Lens),图像传感器(Image
    康谋 2024-12-12 09:45 83浏览
  • 时源芯微——RE超标整机定位与解决详细流程一、 初步测量与问题确认使用专业的电磁辐射测量设备,对整机的辐射发射进行精确测量。确认是否存在RE超标问题,并记录超标频段和幅度。二、电缆检查与处理若存在信号电缆:步骤一:拔掉所有信号电缆,仅保留电源线,再次测量整机的辐射发射。若测量合格:判定问题出在信号电缆上,可能是电缆的共模电流导致。逐一连接信号电缆,每次连接后测量,定位具体哪根电缆或接口导致超标。对问题电缆进行处理,如加共模扼流圈、滤波器,或优化电缆布局和屏蔽。重新连接所有电缆,再次测量
    时源芯微 2024-12-11 17:11 115浏览
  • 首先在gitee上打个广告:ad5d2f3b647444a88b6f7f9555fd681f.mp4 · 丙丁先生/香河英茂工作室中国 - Gitee.com丙丁先生 (mr-bingding) - Gitee.com2024年对我来说是充满挑战和机遇的一年。在这一年里,我不仅进行了多个开发板的测评,还尝试了多种不同的项目和技术。今天,我想分享一下这一年的故事,希望能给大家带来一些启发和乐趣。 年初的时候,我开始对各种开发板进行测评。从STM32WBA55CG到瑞萨、平头哥和平海的开发板,我都
    丙丁先生 2024-12-11 20:14 78浏览
  • 本文介绍瑞芯微RK3588主板/开发板Android12系统下,APK签名文件生成方法。触觉智能EVB3588开发板演示,搭载了瑞芯微RK3588芯片,该开发板是核心板加底板设计,音视频接口、通信接口等各类接口一应俱全,可帮助企业提高产品开发效率,缩短上市时间,降低成本和设计风险。工具准备下载Keytool-ImportKeyPair工具在源码:build/target/product/security/系统初始签名文件目录中,将以下三个文件拷贝出来:platform.pem;platform.
    Industio_触觉智能 2024-12-12 10:27 81浏览
  • RK3506 是瑞芯微推出的MPU产品,芯片制程为22nm,定位于轻量级、低成本解决方案。该MPU具有低功耗、外设接口丰富、实时性高的特点,适合用多种工商业场景。本文将基于RK3506的设计特点,为大家分析其应用场景。RK3506核心板主要分为三个型号,各型号间的区别如下图:​图 1  RK3506核心板处理器型号场景1:显示HMIRK3506核心板显示接口支持RGB、MIPI、QSPI输出,且支持2D图形加速,轻松运行QT、LVGL等GUI,最快3S内开
    万象奥科 2024-12-11 15:42 88浏览
  • 应用环境与极具挑战性的测试需求在服务器制造领域里,系统整合测试(System Integration Test;SIT)是确保产品质量和性能的关键步骤。随着服务器系统的复杂性不断提升,包括:多种硬件组件、操作系统、虚拟化平台以及各种应用程序和服务的整合,服务器制造商面临着更有挑战性的测试需求。这些挑战主要体现在以下五个方面:1. 硬件和软件的高度整合:现代服务器通常包括多个处理器、内存模块、储存设备和网络接口。这些硬件组件必须与操作系统及应用软件无缝整合。SIT测试可以帮助制造商确保这些不同组件
    百佳泰测试实验室 2024-12-12 17:45 74浏览
  • 铁氧体芯片是一种基于铁氧体磁性材料制成的芯片,在通信、传感器、储能等领域有着广泛的应用。铁氧体磁性材料能够通过外加磁场调控其导电性质和反射性质,因此在信号处理和传感器技术方面有着独特的优势。以下是对半导体划片机在铁氧体划切领域应用的详细阐述: 一、半导体划片机的工作原理与特点半导体划片机是一种使用刀片或通过激光等方式高精度切割被加工物的装置,是半导体后道封测中晶圆切割和WLP切割环节的关键设备。它结合了水气电、空气静压高速主轴、精密机械传动、传感器及自动化控制等先进技术,具有高精度、高
    博捷芯划片机 2024-12-12 09:16 87浏览
  • 习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-12 10:13 50浏览
  • 一、SAE J1939协议概述SAE J1939协议是由美国汽车工程师协会(SAE,Society of Automotive Engineers)定义的一种用于重型车辆和工业设备中的通信协议,主要应用于车辆和设备之间的实时数据交换。J1939基于CAN(Controller Area Network)总线技术,使用29bit的扩展标识符和扩展数据帧,CAN通信速率为250Kbps,用于车载电子控制单元(ECU)之间的通信和控制。小北同学在之前也对J1939协议做过扫盲科普【科普系列】SAE J
    北汇信息 2024-12-11 15:45 115浏览
  • 全球智能电视时代来临这年头若是消费者想随意地从各个通路中选购电视时,不难发现目前市场上的产品都已是具有智能联网功能的智能电视了,可以宣告智能电视的普及时代已到临!Google从2021年开始大力推广Google TV(即原Android TV的升级版),其他各大品牌商也都跟进推出搭载Google TV操作系统的机种,除了Google TV外,LG、Samsung、Panasonic等大厂牌也开发出自家的智能电视平台,可以看出各家业者都一致地看好这块大饼。智能电视的Wi-Fi连线怎么消失了?智能电
    百佳泰测试实验室 2024-12-12 17:33 66浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦