C++多线程编程时的数据保护

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

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

#include <iostream>
#include <string>
#include <thread>
#include <vector>

using std::thread;
using std::vector;
using std::cout;
using std::endl;

class Incrementer
{
  private:
    int counter;

  public:
    Incrementer() : counter{0} { };

    void operator()()
    {
      for(int i = 0; i < 100000; i++)
      {
        this->counter++;
      }
    }

    int getCounter() const
    {
      return this->counter;
    }
};

int main()
{
  // Create the threads which will each do some counting
  vector<thread> threads;

  Incrementer counter;

  threads.push_back(thread(std::ref(counter)));
  threads.push_back(thread(std::ref(counter)));
  threads.push_back(thread(std::ref(counter)));

  for(auto &t : threads)
  {
    t.join();
  }

  cout << counter.getCounter() << endl;

  return 0;
}

这个程序的目的就是数数,数到30万,某些傻叉程序员想要优化数数的过程,因此创建了三个线程,使用一个共享变量 counter,每个线程负责给这个变量增加10万计数。

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

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

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

现在让我们来看看程序执行的结果,如果这位傻叉程序员还够聪明的话,他会使用 GCC 4.7 或者更新版本,或者是 Clang 3.1 来进行编译,编译方法:

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

运行结果:

[lucas@lucas-desktop src]$ ./threading_example
218141
[lucas@lucas-desktop src]$ ./threading_example
208079
[lucas@lucas-desktop src]$ ./threading_example
100000
[lucas@lucas-desktop src]$ ./threading_example
202426
[lucas@lucas-desktop src]$ ./threading_example
172209

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

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

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

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

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

正确的做法

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

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

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

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

#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include <mutex>

using std::thread;
using std::vector;
using std::cout;
using std::endl;
using std::mutex;

class Incrementer
{
  private:
    int counter;
    mutex m;

  public:
    Incrementer() : counter{0} { };

    void operator()()
    {
      for(int i = 0; i < 100000; i++)
      {
        this->m.lock();
        this->counter++;
        this->m.unlock();
      }
    }

    int getCounter() const
    {
      return this->counter;
    }
};

int main()
{
  // Create the threads which will each do some counting
  vector<thread> threads;

  Incrementer counter;

  threads.push_back(thread(std::ref(counter)));
  threads.push_back(thread(std::ref(counter)));
  threads.push_back(thread(std::ref(counter)));

  for(auto &t : threads)
  {
    t.join();
  }

  cout << counter.getCounter() << endl;

  return 0;
}

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

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

[lucas@lucas-desktop src]$ ./threading_example
300000
[lucas@lucas-desktop src]$ ./threading_example
300000

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

防范异常

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

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

for(int i = 0; i < 100000; i++)
{
 this->m.lock();
 try
  {
   this->counter++;
   this->m.unlock();
  }
  catch(...)
  {
   this->m.unlock();
   throw;
  }
}

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

继续修改代码:

void operator()()
{
  for(int i = 0; i < 100000; i++)
  {
  lock_guard<mutex> lock(this->m);

  // The lock has been created now, and immediatly locks the mutex
  this->counter++;

  // This is the end of the for-loop scope, and the lock will be
  // destroyed, and in the destructor of the lock, it will
  // unlock the mutex
  }
}

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

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

void long_function()
{
  // some long code

  // Just a pair of curly braces
  {
  // Temp scope, create lock
  lock_guard<mutex> lock(this->m);

  // do some stuff

  // Close the scope, so the guard will unlock the mutex
  }
}
(0)

相关推荐

  • 在ASP.NET 2.0中操作数据之七十一:保护连接字符串及其它设置信息

    导言: ASP.NET应用程序的设置信息通常都存储在一个名为Web.config的XML文件里.在教程的前面部分我们已经好几次修改过Web.config文件了.比如在第一章,我们创建名为Northwind的数据集时,数据库连接字符串信息自动的添加到Web.config文件的<connectionStrings>节点.再后来,在第3章里,我们手动更新了Web.config文件,添加了一个<pages>元素,对所有的ASP.NET页面运用DataWebControls主题. 由于Web

  • Oracle数据库 DGbroker三种保护模式的切换

    1.三种保护模式 – Maximum protection 在Maximum protection下, 可以保证从库和主库数据完全一样,做到zero data loss.事务同时在主从两边提交完成,才算事务完成.如果从库宕机或者网络出现问题,主从库不能通讯,主库也立即宕机.在这种方式下,具有最高的保护等级.但是这种模式对主库性能影响很大,要求高速的网络连接. – Maximum availability 在Maximum availability模式下,如果和从库的连接正常,运行方式等同Maxi

  • ASP.NET Core 数据保护(Data Protection)上篇

    前言  上一篇记录了如何在 Kestrel 中使用 HTTPS(SSL), 也是我们目前项目中实际使用到的. 数据安全往往是开发人员很容易忽略的一个部分,包括我自己.近两年业内也出现了很多因为安全问题导致了很多严重事情发生,所以安全对我们开发人员很重要,我们要对我们的代码的安全负责. 在工作中,我们常常会见到 encode,base64,sha256, rsa, hash,encryption, md5 等,一些人对他们还傻傻分不清楚,也不知道什么时候使用他们,还有一些人认为MD5就是加密算法.

  • ASP.NET Core 数据保护(Data Protection 集群场景)下篇

    前言  接[中篇] ,在有一些场景下,我们需要对 ASP.NET Core 的加密方法进行扩展,来适应我们的需求,这个时候就需要使用到了一些 Core 提供的高级的功能. 本文还列举了在集群场景下,有时候我们需要实现自己的一些方法来对Data Protection进行分布式配置. 加密扩展  IAuthenticatedEncryptor 和 IAuthenticatedEncryptorDescriptor  IAuthenticatedEncryptor是 Data Protection 在

  • 利用DOS命令来对抗U盘病毒保护U盘数据

    U盘的便捷性与大容量的存储性,深受着广大用户的欢迎,几乎每个用户都会人手一个,但就是这么广泛的使用,以至于U盘被病毒悄悄盯上,越来越多的病毒通过电脑.通过文件毁坏重要的数据,为了保护好U盘的数据,灭除U盘病毒成为了用户们的首要任务,下面就教大家一个小技巧,利用DOS命令来对抗U盘病毒. 利用DOS命令删除U盘病毒的步骤: 1.点击"开始→运行",输入"CMD",按回车键 2.打开命令提示符窗口,切换到U盘所在盘符或是中了Autorun.inf病毒的盘符下,依次执行下

  • 设置密码保护的SqlServer数据库备份文件与恢复文件的方法

    设置密码保护SqlServer数据库备份文件! 备份SqlServer数据库 Backup Database [数据库] To disk='c:\mysql'+ replace(replace(replace(replace(CONVERT(varchar, getdate(), 121),'-',''),' ',''),':',''),'.','') +'.bak' With Password = '123',init; 恢复SqlServer数据库 Restore Database [数据库

  • 探究在C++程序并发时保护共享数据的问题

    我们先通过一个简单的代码来了解该问题. 同步问题 我们使用一个简单的结构体 Counter,该结构体包含一个值以及一个方法用来改变这个值: struct Counter { int value; void increment(){ ++value; } }; 然后启动多个线程来修改结构体的值: int main(){ Counter counter; std::vector<std::thread> threads; for(int i = 0; i < 5; ++i){ threads

  • 如何保护MySQL中重要数据的方法

    企业最有价值的资产通常是其数据库中的客户或产品信息.因此,在这些企业中,数据库管理的一个重要部分就是保护这些数据免受外部攻击,及修复软/硬件故障. 在大多数情况下,软硬件故障通过数据备份机制来处理.多数数据库都自带有内置的工具自动完成整个过程,所以这方面的工作相对轻松,也不会出错.但麻烦却来自另一面:阻止外来黑客入侵窃取或破坏数据库中的信息.不幸的是,一般没有自动工具解决这一问题;而且,这需要管理员手工设置障碍来阻止黑客,确保公司数据的安全. 不对数据库进行保护的常见原因是由于这一工作"麻烦&q

  • 保护你的Sqlite数据库(SQLite数据库安全秘籍)

    SQLite无任何限制的授权协议以及支持大部分标准的SQL 92语句,相信会有越来越多的人使用这个数据库. PHP与SQLite的结合就如同当年的ASP与ACCESS结合一样,ACCESS可以遭遇被人恶意下载,SQLite同样不能幸免,因为SQLite也是一个二进制文件,只要WEB能访问到的,就能被下载. ACCESS可以采用一些诡计来防止用户下载,SQLite也可以.下面向大家介绍几种常用的防止SQLite被下载的方法 1.将SQLite放在WEB不能访问到的地方. 有些虚拟主机一般也都会提供

  • 利用MySQL加密函数保护Web网站敏感数据的方法分享

    如果您正在运行使用MySQL的Web应用程序,那么它把密码或者其他敏感信息保存在应用程序里的机会就很大.保护这些数据免受黑客或者窥探者的获取是一个令人关注的重要问题,因为您既不能让未经授权的人员使用或者破坏应用程序,同时还要保证您的竞争优势.幸运的是,MySQL带有很多设计用来提供这种类型安全的加密函数.本文概述了其中的一些函数,并说明了如何使用它们,以及它们能够提供的不同级别的安全. 双向加密 就让我们从最简单的加密开始:双向加密.在这里,一段数据通过一个密钥被加密,只能够由知道这个密钥的人来

随机推荐