文档章节

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

 天下杰论
发布于 2013/12/29 18:51
字数 2020
阅读 124
收藏 0
点赞 0
评论 0

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

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


同步问题

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

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

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++ 线程库中的互斥量来保护你的共享数据。

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


下篇

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

本文转载自:http://billhoo.blog.51cto.com/2337751/1294320

共有 人打赏支持
粉丝 53
博文 400
码字总数 23359
作品 0
沈阳
项目经理
C++11 并发 —— 第一部分:启动线程

C++11 引入一个全新的线程库,包含启动和管理线程的工具,提供了同步(互斥、锁和原子变量)的方法,我将试图为你介绍这个全新的线程库。 如果你要编译本文中的代码,你至少需要一个支持 C+...

红薯 ⋅ 2012/03/22 ⋅ 12

C++11 并发教程第二部分:保护共享数据

在上一篇文章“C++11 并发 —— 第一部分:启动线程”中我们介绍了如何在C++11中编写多线程程序,这些在线程中执行的代码都是独立的,但在实际应用中,我们经常会需要线程去访问一些共享的数...

红薯 ⋅ 2012/03/27 ⋅ 6

C++11 std::unique_lock与std::lock_guard的区别及多线程应用实例

C++11std::uniquelock与std::lockguard的区别及多线程应用实例 C++多线程编程中通常会对共享的数据进行写保护,以防止多线程在对共享数据成员进行读写时造成资源争抢导致程序出现未定义的行为...

moki_oschina ⋅ 04/10 ⋅ 0

浅谈Java并发编程系列(一)—— 如何保证线程安全

线程安全类 保证类线程安全的措施: 不共享线程间的变量; 设置属性变量为不可变变量; 每个共享的可变变量都使用一个确定的锁保护; 保证线程安全的思路: 1. 通过架构设计 通过上层的架构设...

codershamo ⋅ 2017/11/29 ⋅ 0

Java 使用 happen-before 规则实现共享变量的同步操作

前言 熟悉 Java 并发编程的都知道,JMM(Java 内存模型) 中的 happen-before(简称 hb)规则,该规则定义了 Java 多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响。按照官方的...

stateIs0 ⋅ 01/20 ⋅ 0

【C++11 并发编程教程 - Part 1 : thread 初探(bill译)】

C++11 并发编程教程 - Part 1 : thread 初探 注:文中凡遇通用的术语及行话,均不予以翻译。译文有不当之处还望悉心指正。 原文:C++11 Concurrency - Part 1 : Start Threads C++11 引入了一...

技术小胖子 ⋅ 2017/11/09 ⋅ 0

java并发编程线程安全

编写线程安全的代码实质就是管理对状态的访问,而且通常是共享的。可变的状态,对象的状态就是数据,存储在状态变量中,比如实例域,或者静态域,同时还包含了其它附属的域,例如hashmap的状...

攀爬的小瓜牛 ⋅ 2014/07/11 ⋅ 0

C++11 并发编程教程 - Part 1 : thread 初探(bill译)

C++11 引入了一个新的线程库,包含了用于启动、管理线程的诸多工具,与此同时,该库还提供了包括互斥量、锁、原子量等在内的同步机制。在这个系列的教程中,我将尝试向大家展示这个新库提供的...

天下杰论 ⋅ 2013/12/29 ⋅ 0

微软发布Axum v0.1版本

Axum是微软开发的并行编程语言,目前还处在试验阶段。现在,Axum的一个初期版本(v0.1)已经发布并提供下载。您需要安装Visual Studio 2008才能使用Axum。 微软DevLabs表示此次发布的版本,其...

老枪 ⋅ 2009/05/14 ⋅ 0

并发一:JAVA并发模型

一、并发 并发程序是指在运行中有两个及以上的任务同时在处理,与之相关的概念并行,是指在运行中有两个及以上的任务同时执行,差别是在于处理和执行。在单核CUP中两个及以上任务的处理方式是...

wangjie2016 ⋅ 2017/05/18 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

idea 整合 vue 启动

刚学习Vue 搭建了一个项目 只能命令启动 Idea里面不会启动 尝试了一下修改启动的配置 如下: 1.首先你要保证你的package.json没有修改过 具体原因没有看 因为我改了这个name的值 就没办法启动...

事儿爹 ⋅ 24分钟前 ⋅ 0

数据仓库技术概述(一看就是架构师写的,对我极其有用)

ETL,是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取(extract)、交互转换(transform)、加载(load)至目的端的过程。ETL一词较常用在数据仓库,但其对象并不限于...

gulf ⋅ 26分钟前 ⋅ 0

redis在windows环境的后台运行方法

在后台运行,首先需要安装redis服务,命令为 redis-server.exe --service-install redis.windows.conf --loglevel verbose 启动,命令为 redis-server --service-start 停止,命令为 redis-...

程序羊 ⋅ 27分钟前 ⋅ 0

比特币现金开发者提出新的交易订单规则

本周,四位比特币现金的四位开发者和研究员:Joannes Vermorel(Lokad),AmaurySéchet(比特币ABC),Shammah Chancellor(比特币ABC)和Tomas van der Wansem(Bitcrust)共同发表了一篇关...

lpy411 ⋅ 31分钟前 ⋅ 0

vue获取input输入框的数据

用惯了jQuery,突然使用vue感觉很不习惯,有很多不同的地方,感觉是两个不同的思想来写前端的代码。jQuery是使用选择器($)选取DOM对象,对其进行赋值、取值、事件绑定等操作。而Vue则是通过...

王子城 ⋅ 33分钟前 ⋅ 0

竟然这就是面向对象的游戏设计?!

从程序角度考虑,许多 JavaScript 都基于循环和大量的 if/else 语句。在本文中,我们可了解一种更聪明的做法 — 在 JavaScript 游戏中使用面向对象来设计。本文将概述原型继承和使用 JavaSc...

柳猫 ⋅ 38分钟前 ⋅ 2

git cmd git bash

刚用到了Git,看到windows环境下有两个命令输入窗口 第一个是可视化图形界面,第二个是CMD,第三个是Bash。 Git中的Bash是基于CMD的,在CMD的基础上增添一些新的命令与功能。所以建议在使用的...

东东笔记 ⋅ 40分钟前 ⋅ 0

分布式系统CAP和Base

1、分布式系统 1.1 简介 由多台计算机和通信的软件组件通过计算机网络连接(本地网络或广域网)组成。分布式系统是建立在网络之上的软件系统。正是因为软件的特性,所以分布式系统具有高度的...

xixingzhe ⋅ 51分钟前 ⋅ 0

查看磁盘占用情况

记一次jenkins构建失败的问题 Build step 'Send build artifacts over SSH' changed build result to UNSTABLE 网上查资料都没明确表明是什么错,回忆之前处理这样的问题。第一时间想到的是不...

ManderSF ⋅ 53分钟前 ⋅ 0

数据库管理提速:SQL解析的探索与应用

前言: SQL解析是一项复杂的技术,一般都是由数据库厂商来掌握,当然也有公司专门提供SQL解析的API。SQL解析与优化是属于编译器范畴,和C语言等其他语言的解析没有本质的区别。其中分为词法分...

java高级架构牛人 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部