C++11 多线程:数据保护

C++11 多线程:数据保护

转自:http://www.oschina.net/question/12_59878


编写多线程程序时,多个线程同时访问某个共享资源,会导致同步的问题,这篇文章中我们将介绍 C++11 多线程编程中的数据保护。

数据丢失

让我们从一个简单的例子开始,请看如下代码:

01 #include
02 #include
03 #include
04 #include
05  
06 using std::thread;
07 using std::vector;
08 using std::cout;
09 using std::endl;
10  
11 class Incrementer
12 {
13     private:
14         int counter;
15  
16     public:
17         Incrementer() : counter{0} { };
18  
19         void operator()()
20         {
21             for(int i = 0; i < 100000; i++)
22             {
23                 this->counter++;
24             }
25         }
26  
27         int getCounter() const
28         {
29             return this->counter;
30         }      
31 };
32  
33 int main()
34 {
35     // Create the threads which will each do some counting
36     vector<thread> threads;
37  
38     Incrementer counter;
39  
40     threads.push_back(thread(std::ref(counter)));
41     threads.push_back(thread(std::ref(counter)));
42     threads.push_back(thread(std::ref(counter)));
43  
44     for(auto &t : threads)
45     {
46         t.join();
47     }
48  
49     cout << counter.getCounter() << endl;
50  
51     return 0;
52 }

这个程序的目的就是数数,数到30万,因此创建了三个线程,使用一个共享变量 counter,每个线程负责给这个变量增加10万计数。

这段代码创建了一个名为 Incrementer 的类,该类包含一个私有变量 counter,其构造器非常简单,只是将 counter 设置为 0.

紧接着是一个操作符重载,这意味着这个类的每个实例都是被当作一个简单函数来调用的。一般我们调用类的某个方法时会这样 object.fooMethod(),但现在你实际上是直接调用了对象,如 object(). 因为我们是在操作符重载函数中将整个对象传递给了线程类。最后是一个 getCounter 方法,返回 counter 变量的值。

再下来是程序的入口函数 main(),我们创建了三个线程,不过只创建了一个 Incrementer 类的实例,然后将这个实例传递给三个线程,注意这里使用了 std::ref ,这相当于是传递了实例的引用对象,而不是对象的拷贝。

现在让我们来看看程序执行的结果,使用 GCC 4.7 或者更新版本,或者是 Clang 3.1 来进行编译,编译方法:

1 g++ -std=c++11 -lpthread -o threading_example main.cpp

运行结果:

01 [lucas@lucas-desktop src]$ ./threading_example
02 218141
03 [lucas@lucas-desktop src]$ ./threading_example
04 208079
05 [lucas@lucas-desktop src]$ ./threading_example
06 100000
07 [lucas@lucas-desktop src]$ ./threading_example
08 202426
09 [lucas@lucas-desktop src]$ ./threading_example
10 172209

但等等,不对啊,程序并没有数数到30万,有一次居然只数到10万,为什么会这样呢?好吧,加1操作对应实际的处理器指令其实包括:

1 movl    counter(%rip), %eax
2 addl    $1, %eax
3 movl    %eax, counter(%rip)

首个指令将装载 counter 的值到 %eax 寄存器,紧接着寄存器的值增1,然后将寄存器的值移给内存中 counter 所在的地址。

我听到你在嘀咕:这不错,可为什么会导致数数错误的问题呢?嗯,还记得我们以前说过线程会共享处理器,因为只有单核。因此在某些点上,一个线程会依照指令执行完成,但在很多情况下,操作系统会对线程说:时间结束了,到后面排队再来,然后另外一个线程开始执行,当下一个线程开始执行时,它会从被暂停的那个位置开始执行。所以你猜会发生什么事,当前线程正准备执行寄存器加1操作时,系统把处理器交给另外一个线程?

我真的不知道会发生什么事,可能我们在准备加1时,另外一个线程进来了,重新将 counter 值加载到寄存器等多种情况的产生。谁也不知道到底发生了什么。

正确的做法

解决方案就是要求同一个时间内只允许一个线程访问共享变量。这个可通过 std::mutex 类来解决。当线程进入时,加锁、执行操作,然后释放锁。其他线程想要访问这个共享资源必须等待锁释放。

互斥(mutex) 是操作系统确保锁和解锁操作是不可分割的。这意味着线程在对互斥量进行锁和解锁的操作是不会被中断的。当线程对互斥量进行锁或者解锁时,该操作会在操作系统切换线程前完成。

而最好的事情是,当你试图对互斥量进行加锁操作时,其他的线程已经锁住了该互斥量,那你就必须等待直到其释放。操作系统会跟踪哪个线程正在等待哪个互斥量,被堵塞的线程会进入 "blocked on m" 状态,意味着操作系统不会给这个堵塞的线程任何处理器时间,直到互斥量解锁,因此也不会浪费 CPU 的循环。如果有多个线程处于等待状态,哪个线程最先获得资源取决于操作系统本身,一般像 Windows 和 Linux 系统使用的是 FIFO 策略,在实时操作系统中则是基于优先级的。

现在让我们对上面的代码进行改进:

01 #include
02 #include
03 #include
04 #include
05 #include
06  
07 using std::thread;
08 using std::vector;
09 using std::cout;
10 using std::endl;
11 using std::mutex;
12  
13 class Incrementer
14 {
15     private:
16         int counter;
17         mutex m;
18  
19     public:
20         Incrementer() : counter{0} { };
21  
22         void operator()()
23         {
24             for(int i = 0; i < 100000; i++)
25             {
26                 this->m.lock();
27                 this->counter++;
28                 this->m.unlock();
29             }
30         }
31  
32         int getCounter() const
33         {
34             return this->counter;
35         }  
36 };
37  
38 int main()
39 {
40     // Create the threads which will each do some counting
41     vector<thread> threads;
42  
43     Incrementer counter;
44  
45     threads.push_back(thread(std::ref(counter)));
46     threads.push_back(thread(std::ref(counter)));
47     threads.push_back(thread(std::ref(counter)));
48  
49     for(auto &t : threads)
50     {
51         t.join();
52     }
53  
54     cout << counter.getCounter() << endl;
55  
56     return 0;
57 }

注意代码上的变化:我们引入了 mutex 头文件,增加了一个 m 的成员,类型是 mutex,在 operator()() 中我们锁住互斥量 m 然后对 counter 进行加1操作,然后释放互斥量。

再次执行上述程序,结果如下:

1 [lucas@lucas-desktop src]$ ./threading_example
2 300000
3 [lucas@lucas-desktop src]$ ./threading_example
4 300000

这下数对了。不过在计算机科学中,没有免费的午餐,使用互斥量会降低程序的性能,但这总比一个错误的程序要强吧。

防范异常

当对变量进行加1操作时,是可能会发生异常的,当然在我们这个例子中发生异常的机会微乎其微,但是在一些复杂系统中是极有可能的。上面的代码并不是异常安全的,当异常发生时,程序已经结束了,可是互斥量还是处于锁的状态。

为了确保互斥量在异常发生的情况下也能被解锁,我们需要使用如下代码:

01 for(int i = 0; i < 100000; i++)
02  {
03  this->m.lock();
04  try
05  {
06      this->counter++;
07      this->m.unlock();
08  }
09  catch(...)
10  {
11      this->m.unlock();
12      throw;
13  }
14  }

但是,这代码太多了,而只是为了对互斥量进行加锁和解锁。没关系,我知道你很懒,因此推荐个更简单的单行代码解决方法,就是使用 std::lock_guard 类。这个类在创建时就锁定了 mutex 对象,然后在结束时释放。

继续修改代码:

01 void operator()()
02 {
03     for(int i = 0; i < 100000; i++)
04     {
05     lock_guard lock(this->m);
06  
07     // The lock has been created now, and immediatly locks the mutex
08     this->counter++;
09  
10     // This is the end of the for-loop scope, and the lock will be
11     // destroyed, and in the destructor of the lock, it will
12     // unlock the mutex
13     }
14 }

上面代码已然是异常安全了,因为当异常发生时,将会调用 lock 对象的析构函数,然后自动进行互斥量的解锁。

记住,请使用放下代码模板来编写:

01 void long_function()
02 {
03     // some long code
04  
05     // Just a pair of curly braces
06     {
07     // Temp scope, create lock
08     lock_guard lock(this->m);
09  
10     // do some stuff
11  
12     // Close the scope, so the guard will unlock the mutex
13     }
14 }


发布了202 篇原创文章 · 获赞 122 · 访问量 150万+

        
发表评论
还能输入1000个字符

多线程中数据的并发访问与保护

12-16 阅读数 6387

在多线程编程中,不可避免地要对一些共享的数据进行访问。由于线程之间对共享数据的访问是独立的,任何一个线程都可对共享数据进行访问和修改,且它们之间是异步并发进行的,特别是当需要对共享数据进行修改时,就会... 博文 来自: 山庄来客的专栏

多线程编程的关键是要保护数据

06-14 阅读数 86

多线程的程序与单线程相比,主要的特点是“同一份数据可能会被多个线程访问,而每个线程执行的程序代码是有可能被从中间中断的,因此如果处理不好,数据的改变可能不是我们期望的那样。”为了让数据是按期望的那样被... 博文 来自: chenm91的专栏

thread类使用笔记

01-06 阅读数 357

前言        C++98/03不支持多线程编程,所以必须借助第三方库(如pthreads、boost::thread)或者目标操作系统中的多线程API(如Windows)。自C++11开始包含了... 博文 来自: 代码人生

C++ push方法与push_back方法 浅析

07-12 阅读数 4万+

push与push_back是STL中常见的方法,都是向数据结构中添加元素。初识STL,对于添加元素的方法以产生混淆,这里暂对两种方法作出比较分析。此外,本文还将简述push对应的stack与queu... 博文 来自: 稚枭天卓

多线程共享数据保护

12-18

请教下多线程问题,如标题“多线程共享数据保护”。也看了以前一些朋友提的问题,以及回复。感觉还没有找到正确答案,所有再拿出来提下。 问题的提出:需求是通讯后的数据保存,我采用2个线程一个负责通讯,一个负 论坛

C++多线程Thread的使用

09-06 阅读数 116

std::threadthread的构造函数支持4中函数普通函数函数对象类的成员函数lamda表达式#include #include #include #include #include void ... 博文 来自: daodan81362188669的专栏

线程池 将任务push_back到list中的问题

06-02

最近在用到线程池,于是在codeproject上找到一个(http://www.codeproject.com/KB/threads/win32threadpool.aspx) 用这个线程池有一个问题 论坛

C++ 多线程的数据保护机制

02-18 阅读数 2219

C++ 多线程的数据保护机制   同许多线程API一样,C++0x用互斥来保护共享数据。有四种互斥类型:Non-recursive (std::mutex)Recursive (std::recurs... 博文 来自: caimagic的专栏

C++ 多线程并发控制——互斥锁 pthread_mutex

03-30 阅读数 3万+

问题描述:有两个线程,主线程负责接收数据,并暂时保存在内存中,当内存中数量达到一定数据量时,批量提交到oracle中;另一个线程作为提交线程,定时检查一遍,不论内存中数据量达到多少,定期将数据提交到o... 博文

C++应用多线程提高并发性

12-19

请大家谈谈你在项目应用多线程来提高并发性的场景? 有哪些好的技巧? 在什么情况下应用多线程? 有什么好的建议? 论坛

C++多线程编程视频教程(C++11多线程并发)

03-06

C++多线程编程视频教程(C++11多线程并发) 第一节什么是多线程? 第二节多线程API详解(一) 第三节多线程API详解(二) 第四节多线程API详解(三) 第五节多线程模拟火车站售票 第六节多线 论坛

		
山庄来客关注
山庄来客

224篇文章

排名:5000+

chenm91关注
chenm91

7篇文章

排名:千里之外

苦逼的IT男关注
苦逼的IT男

165篇文章

排名:千里之外

稚枭天卓关注
稚枭天卓

597篇文章

排名:953

Java中实现多线程并发的几种安全机制

12-12 阅读数 1226

1.对共享资源进行加锁处理2.进行令牌抢夺机制(token(),Synchronize())3.包装线程不安全类,static Set synchronizedSet(Set s)4.threadL... 博文 来自: BabyBirdToFly的博客

C++11 并发编程教程 - Part 1 : thread 初探

08-03 阅读数 141

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

C++11并发编程(一)——初始C++11多线程库

08-05 阅读数 2506

1 前言   C++11标准在标准库中为多线程提供了组件,这意味着使用C++编写与平台无关的多线程程序成为可能,而C++程序的可移植性也得到了有力的保证。   在之前我们主要使用的多线程库要么是属于某... 博文 来自: 无鞋童鞋的博客

C++11 并发与多线程(一)

01-26 阅读数 6166

std::thread 类1.1, 什么叫并发 concurrency?一遍走路一边说话;你打球我游泳单核计算机上的并发是个假象,其实只是任务切换(task switching)需要上下文切换 多处理... 博文 来自: 嘿,逮到一只大码农

多线程访问数据库时应该怎么进行临界区保护呢?

01-06

用的是一个全局的ConnectionPtr和一个全局的RecordsetPtr 在主线程中建立了与数据库的连接 工作线程中对数据库进行读写的操作 对数据库的操作是做在函数中的,然后工作线程调用这些函数 论坛


C++并发编程2——为保护数据加锁(一)

09-29 阅读数 4734

在应届生面试的时候,很多面试官都会问——“多线程如何共享资源”。在操作系统层面上可以给出若干关键词答案,但是在语言层面,这个问题考虑的就没有那么简单了。... 博文 来自: 小熊糖否——木子兮的自留地

CVI多线程数据保护(多个线程操作同一安全变量)

10-11 阅读数 1639

有网友提出多个线程操作多个变量不能明确显示是否做到异步操作,现在我们来看下多个线程操作同一变量,是不是有序的进行呢?#include #include #include int CVICALLBA... 博文 来自: 思维逆逝

最新文章

  • js中的模块
  • CI的csrf的使用说明(2)
  • CI的csrf的使用说明
  • 防止网页被嵌入框架的代码
  • VC++ 使用WebBrowser控件中html文件以资源形式加载

分类专栏

  • CI 5篇
  • VC 3篇
  • 小马技术-Vue.js入门教学 3篇
  • BIOS 3篇
  • boost 2篇
  • C/C++ 85篇
  • gcc
  • linux 67篇
  • MeeGo 3篇
  • QT 54篇
  • QT浏览器 2篇
  • WINDOWS 24篇
  • 个人作品 17篇
  • 大话人生 8篇
  • 实战DeviceIoControl 7篇
  • 数据恢复 20篇
  • 计算机技术 18篇
  • 设计模式 3篇
  • 汇编 1篇
  • loki 1篇
  • Linux Shell 9篇
  • vim
  • C++11 30篇
  • WEB技术 60篇
  • PHP 48篇
  • python 1篇
  • js 12篇
  • virus 1篇

展开

归档

  • 2020年3月 2篇
  • 2020年2月 10篇
  • 2020年1月 2篇
  • 2019年12月 1篇
  • 2019年11月 2篇
  • 2019年2月 2篇
  • 2018年12月 1篇
  • 2018年10月 6篇
  • 2018年9月 1篇
  • 2018年8月 9篇
  • 2018年7月 16篇
  • 2018年6月 11篇
  • 2018年5月 10篇
  • 2018年3月 2篇
  • 2018年2月 1篇
  • 2018年1月 2篇
  • 2017年10月 1篇
  • 2017年9月 9篇
  • 2017年8月 2篇
  • 2017年7月 4篇
  • 2014年6月 1篇
  • 2014年5月 2篇
  • 2013年11月 6篇
  • 2013年10月 1篇
  • 2013年9月 2篇
  • 2013年7月 6篇
  • 2013年6月 2篇
  • 2013年5月 5篇
  • 2013年4月 4篇
  • 2013年3月 1篇
  • 2013年2月 1篇
  • 2013年1月 2篇
  • 2012年12月 3篇
  • 2012年11月 1篇
  • 2012年10月 2篇
  • 2012年9月 22篇
  • 2012年8月 2篇
  • 2012年7月 6篇
  • 2012年6月 6篇
  • 2012年5月 27篇
  • 2012年4月 13篇
  • 2012年3月 6篇
  • 2012年2月 11篇
  • 2012年1月 7篇
  • 2011年12月 4篇
  • 2011年11月 6篇
  • 2011年10月 3篇
  • 2011年9月 4篇
  • 2011年8月 5篇
  • 2011年6月 5篇
  • 2011年5月 3篇
  • 2011年4月 6篇
  • 2011年3月 11篇
  • 2011年2月 5篇
  • 2011年1月 6篇
  • 2010年12月 3篇
  • 2010年11月 3篇
  • 2010年10月 3篇
  • 2010年9月 10篇
  • 2010年8月 1篇
  • 2010年7月 8篇
  • 2010年6月 6篇
  • 2010年5月 11篇
  • 2010年4月 11篇
  • 2010年3月 7篇
  • 2010年2月 4篇
  • 2010年1月 11篇
  • 2009年12月 11篇
  • 2009年11月 19篇
  • 2009年10月 4篇

展开

最新评论

  • QT控件中的文本对齐的StyleS...
  • JavaScript的沙箱模式
  • 宝塔面板卸载
  • udev(九)-- 写个程序检测我...
  • Award bios Editor...

你可能感兴趣的:(C++,C/C++)