【C++11 并发编程教程 - Part 2 : 保护共享数据(bill译)】

简介:

C++11 并发编程教程 - Part 2 : 保护共享数据

注:文中凡遇通用的术语及行话,均不予以翻译。译文有不当之处还望悉心指正。

原文:C++11 Concurrency - Part 2 : Protect shared data


   上一篇文章我们讲到如何启动一些线程去并发地执行某些操作,虽然那些在线程里执行的代码都是独立的,但通常情况下,你都会在这些线程之间使用到共享数据。一旦你这么做了,就面临着一个新的问题 —— 同步。

   下面让我们用示例来阐释“同步”是个什么问题。


同步问题

   我们就拿一个简单的计数器作为示例吧。这个计数器是一个结构体,他拥有一个计数变量,以及增加或减少计数的函数,看起来像这个样子:

   [译注:原文 Counter 的 value 并未初始化,其初始值随机,读者可自行初始化为 ]

1
2
3
4
5
6
struct  Counter {
     int  value;
     void  increment(){
         ++value;
     }
};

   这并没什么稀奇的,下面让我们来启动一些线程来增加计数器的计数吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int  main(){
     Counter counter;
     std::vector<std:: thread > threads;
     for ( int  i = 0; i < 5; ++i){
         threads.push_back(std:: thread ([&counter](){
             for ( int  i = 0; i < 100; ++i){
                 counter.increment();
             }
         }));
     }
     for ( auto thread  : threads){
         thread .join();
     }
     std::cout << counter.value << std::endl;
     return  0;
}


   [译注:bill的测试环境下,上述代码始终输出 500,读者可将外层 for 循环条件改为 i < 100,内层 for 循环条件改为 i < 99999 以观察实验结果]

   同样的,也没什么新花样,我们只是启动了 5 个线程,每个线程都让计数器增加 100 次而已。等这一工作结束,我们就打印计数器最后的数值。

   如果运行这一程序,我们理所当然的期望运行结果是 500,但事与愿违,没人能保证这个程序最终输出什么。下面是在我的机器上得到的一些结果:

1
2
3
4
5
6
442
500
477
400
422
487


   问题的根源在于计数器的 increment() 并非原子操作,而是由 3 个独立的操作组成的:

       1. 读取 value 变量的当前值。

       2. 将读取的当前值加 1

       3. 将加 1 后的值写回 value 变量。

   当你以单线程运行上述代码时,就不会出现任何问题,上述三个步骤会按照顺序依次执行。但是一旦你身处多线程环境,情况就会变得糟糕起来,考虑如下执行顺序:


       1. 线程a:读取 value 的当前值,得到值为 0。加1。因此 value = 1。[译注:此时 1 并没有写回 value 内存,原文“value = 1”仅作逻辑意义,下同]

       2. 线程b读取 value 的当前值,得到值为 0。加1。因此 value = 1

       3. 线程a:将 1 写回 value 内存并返回 1

       4. 线程b:将 1 写回 value 内存并返回 1


   这种情况源于线程间的 interleavingInterleaving 描述了多线程同时执行几句代码的各种情况。就算仅仅只有两个线程同时执行这三个操作,也会存在很多可能的 interleaving。当你有许多线程同时执行多个操作时,要想枚举出所有interleaving,几乎是不可能的。而且如果线程在执行单个操作的不同指令之间被抢占,也会导致 interleaving 的发生。

   目前有许多可以解决这一问题的方案:

  • Semaphores

  • Atomic references

  • Monitors

  • Condition codes

  • Compare and swap

  • etc.

   就本文而言,我们将学习如何使用 Semaphores 去解决这一问题。事实上,我们仅仅使用了 Semaphores 中比较特殊的一种 —— 互斥量。互斥量是一个特殊的对象,在同一时刻只有一个线程能够得到该对象上的锁。借助互斥量这种简而有力的性质,我们便可以解决线程同步问题。


使用互斥量保证 Counter 的线程安全

   在 C++11 的线程库中,互斥量被放置于头文件 <mutex>,并以 std::mutex 类加以实现。互斥量有两个重要的函数:lock() 和 unlock()。顾名思义,前者使当前线程尝试获取互斥量的锁,后者则释放已经获取的锁。lock() 函数是阻塞式的,线程一旦调用 lock(),就会一直阻塞直到该线程获得对应的锁。

   为了使我们的计数器具备线程安全性,我们需要对其添加 std::mutex 成员,并在成员函数中对互斥量进行 lock()/unlock() 调用。

1
2
3
4
5
6
7
8
9
10
struct  Counter {
     std::mutex mutex;
     int  value;
     Counter() : value(0) {}
     void  increment(){
         mutex.lock();
         ++value;
         mutex.unlock();
     }
};


   如果我们现在再次运行之前的测试程序,我们将始终得到正确的输出:500。


异常与锁

现在让我们来看看另外一种情况会发生什么。假设现在我们的计数器拥有一个 derement() 操作,当  value 被减为 0时抛出一个异常: 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct  Counter {
     int  value;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
     Counter() : value(0) {}
     void  increment(){
         ++value;
     }
     void  decrement(){
         if (value == 0){
             throw  "Value cannot be less than 0" ;
         }
         --value;
     }
};

   假设你想在不更改上述代码的前提下为其提供线程安全性,那么你需要为其创建一个 Wrapper 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct  ConcurrentCounter {
     std::mutex mutex;
     Counter counter;
     void  increment(){
         mutex.lock();
         counter.increment();
         mutex.unlock();
     }
     void  decrement(){
         mutex.lock();
         counter.decrement();  
         mutex.unlock();
     }
};

   这个 Wrapper 将在大多数情况下正常工作,然而一旦 decrement() 抛出异常,你就遇到大麻烦了,当异常被抛出时,unlock() 函数将不会被调用,这将导致本线程获得的锁不被释放,你的程序也就顺理成章的被永久阻塞了。为了修复这一问题,你需要使用 try/catch 块以保证在抛出任何异常之前释放获得的锁。

1
2
3
4
5
6
7
8
9
10
void  decrement(){
     mutex.lock();
     try  {
         counter.decrement();
     catch  (std::string e){
         mutex.unlock();
         throw  e;
     }
     mutex.unlock();
}


   代码并不复杂,但是看起来却很丑陋。试想一下,你现在的函数拥有 10 个返回点,那么你就需要在每个返回点前调用 unlock() 函数,而忘掉其中的某一个的可能性是非常大的。更大的风险在于你又添加了新的函数返回点,却没有对应地添加 unlock()。下一节将给出解决此问题的好办法。


锁的自动管理

   当你想保护整个代码段(就本文而言是一个函数,但也可以是某个循环体或其他控制结构[译注:即一个作用域])免受多线程的侵害时,有一个办法将有助于防止忘记释放锁:std::lock_guard

   这个类是一个简单、智能的锁管理器。当 std::lock_guard 实例被创建时,它自动地调用互斥量的 lock() 函数,当该实例被销毁时,它也顺带释放掉获得的锁。你可以像这样使用它:

1
2
3
4
5
6
7
8
9
10
11
12
struct  ConcurrentSafeCounter {
     std::mutex mutex;
     Counter counter;
     void  increment(){
         std::lock_guard<std::mutex> guard(mutex);
         counter.increment();
     }
     void  decrement(){
         std::lock_guard<std::mutex> guard(mutex);
         counter.decrement();
     }
};


   代码变得更整洁了不是吗?

   使用这种方法,你无须绷紧神经关注每一个函数返回点是否释放了锁,因为这个操作已经被 std::lock_guard 实例的析构函数接管了。


总结

   现在我们结束了短暂的 Semaphores 之旅。在本章中你学习了如何使用 C++ 线程库中的互斥量来保护你的共享数据。

   但有一点请牢记:锁机制会带来效率的降低。的确,一旦使用锁,你的部分代码就变得有序[译注:非并发]了。如果你想要设计一个高度并发的应用程序,你将会用到其他一些比锁更好的机制,但他们已不属于本文的讨论范畴。


下篇

   在本系列的下一篇文章中,我将谈及关于互斥量的一些进阶概念,并介绍如何使用条件变量去解决一些并发编程问题。




     本文转自Bill_Hoo 51CTO博客,原文链接:http://blog.51cto.com/billhoo/1294320,如需转载请自行联系原作者






相关文章
|
4月前
|
存储 监控 算法
基于 C++ 哈希表算法实现局域网监控电脑屏幕的数据加速机制研究
企业网络安全与办公管理需求日益复杂的学术语境下,局域网监控电脑屏幕作为保障信息安全、规范员工操作的重要手段,已然成为网络安全领域的关键研究对象。其作用类似网络空间中的 “电子眼”,实时捕获每台电脑屏幕上的操作动态。然而,面对海量监控数据,实现高效数据存储与快速检索,已成为提升监控系统性能的核心挑战。本文聚焦于 C++ 语言中的哈希表算法,深入探究其如何成为局域网监控电脑屏幕数据处理的 “加速引擎”,并通过详尽的代码示例,展现其强大功能与应用价值。
98 2
|
5月前
|
存储 C++
UE5 C++:自定义Http节点获取Header数据
综上,通过为UE5创建一个自定义HTTP请求类并覆盖GetResult方法,就能成功地从HTTP响应的Header数据中提取信息。在项目中使用自定义类,不仅可以方便地访问响应头数据,也可随时使用这些信息。希望这种方法可以为你的开发过程带来便利和效益。
200 35
|
6月前
|
IDE 编译器 项目管理
Dev-C++保姆级安装教程:Win10/Win11环境配置+避坑指南(附下载验证)
Dev-C++ 是一款专为 Windows 系统设计的轻量级 C/C++ 集成开发环境(IDE),内置 MinGW 编译器与调试器,支持代码高亮、项目管理等功能。4.9.9 版本作为经典稳定版,适合初学者和教学使用。本文详细介绍其安装流程、配置方法、功能验证及常见问题解决,同时提供进阶技巧和扩展学习资源,帮助用户快速上手并高效开发。
|
7月前
|
算法 Serverless 数据处理
从集思录可转债数据探秘:Python与C++实现的移动平均算法应用
本文探讨了如何利用移动平均算法分析集思录提供的可转债数据,帮助投资者把握价格趋势。通过Python和C++两种编程语言实现简单移动平均(SMA),展示了数据处理的具体方法。Python代码借助`pandas`库轻松计算5日SMA,而C++代码则通过高效的数据处理展示了SMA的计算过程。集思录平台提供了详尽且及时的可转债数据,助力投资者结合算法与社区讨论,做出更明智的投资决策。掌握这些工具和技术,有助于在复杂多变的金融市场中挖掘更多价值。
236 12
|
7月前
|
存储 监控 算法
公司监控上网软件架构:基于 C++ 链表算法的数据关联机制探讨
在数字化办公时代,公司监控上网软件成为企业管理网络资源和保障信息安全的关键工具。本文深入剖析C++中的链表数据结构及其在该软件中的应用。链表通过节点存储网络访问记录,具备高效插入、删除操作及节省内存的优势,助力企业实时追踪员工上网行为,提升运营效率并降低安全风险。示例代码展示了如何用C++实现链表记录上网行为,并模拟发送至服务器。链表为公司监控上网软件提供了灵活高效的数据管理方式,但实际开发还需考虑安全性、隐私保护等多方面因素。
101 0
公司监控上网软件架构:基于 C++ 链表算法的数据关联机制探讨
|
8月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
124 5
|
11月前
|
算法 数据挖掘 Shell
「毅硕|生信教程」 micromamba:mamba的C++实现,超越conda
还在为生信软件的安装配置而烦恼?micromamba(micromamba是mamba包管理器的小型版本,采用C++实现,具有mamba的核心功能,且体积更小,可以脱离conda独立运行,更易于部署)帮你解决!
365 1
|
11月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
347 1
|
11月前
|
Linux C语言 C++
vsCode远程执行c和c++代码并操控linux服务器完整教程
这篇文章提供了一个完整的教程,介绍如何在Visual Studio Code中配置和使用插件来远程执行C和C++代码,并操控Linux服务器,包括安装VSCode、安装插件、配置插件、配置编译工具、升级glibc和编写代码进行调试的步骤。
1955 0
vsCode远程执行c和c++代码并操控linux服务器完整教程
|
存储 算法 C++
C++ STL应用宝典:高效处理数据的艺术与实战技巧大揭秘!
【8月更文挑战第22天】C++ STL(标准模板库)是一组高效的数据结构与算法集合,极大提升编程效率与代码可读性。它包括容器、迭代器、算法等组件。例如,统计文本中单词频率可用`std::map`和`std::ifstream`实现;对数据排序及找极值则可通过`std::vector`结合`std::sort`、`std::min/max_element`完成;而快速查找字符串则适合使用`std::set`配合其内置的`find`方法。这些示例展示了STL的强大功能,有助于编写简洁高效的代码。
180 2