多线程编程时的数据保护,程序并发时保护共享数据的问题

 在编写多线程程序时,五个线程同一时候做客有个别分享财富,会促成同步的难点,那篇小说中大家将介绍
C++11 八线程编制程序中的数据尊崇。
数据遗失

 大家先经过多个粗略的代码来询问该难点。
同步问题

让大家从三个不难的例证开首,请看如下代码:
 

小编们利用一个粗略的布局体
Counter,该结构体满含三个值以至三个艺术用来更改这一个值:
 

#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;
}
struct Counter {
  int value;

  void increment(){
    ++value;
  }
};

这些程序的指标即是数数,数到30万,有些傻叉技师想要优化数数的经过,因而创设了八个线程,使用多少个分享变量
counter,每一个线程担任给这么些变量扩大10万计数。

下一场运营多个线程来校正结构体的值:

这段代码创立了三个名字为 Incrementer 的类,该类富含二个民用变量
counter,其构造器极度轻便,只是将 counter 设置为 0.

 

随之是三个操作符重载,那意味这一个类的各种实例都是被当做二个简约函数来调用的。日常大家调用类的有些方法时会那样
object.fooMethod(),但现行反革命您实乃从来调用了对象,如object().
因为大家是在操作符重载函数元帅整个对象传递给了线程类。最终是叁个getCounter 方法,重回 counter 变量的值。

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;
}

再下来是前后相继的入口函数 main(),大家创造了多个线程,可是只开创了八个Incrementer 类的实例,然后将以此实例传递给多个线程,注意这里运用了
std::ref ,这一定于是传递了实例的引用对象,实际不是目标的正片。

笔者们运营了5个线程来扩张计数器的值,种种线程扩充了96回,然后在线程甘休时打字与印刷计数器的值。

现行反革命让大家来拜会程序试行的结果,假如那位傻叉程序员还够聪明的话,他会使用
GCC 4.7 恐怕更新版本,只怕是 Clang 3.1 来进展编写翻译,编译方法:
 

但大家运营这一个程序的时候,大家是愿意它会承诺500,但并不是如此,没人能不为已甚知道程序将打字与印刷什么结果,上面是在本身机器上运维后打字与印刷的数据,并且每一次都不可同日而道:
 

g++ -std=c++11 -lpthread -o threading_example main.cpp
442
500
477
400
422
487

运行结果:

主题材料的由来在于退换计数器值并非贰个原子操作,须求经过下边多个操作才干造成二遍计数器的扩大:

 

  •     首先读取 value 的值
  •     然后将 value 值加1
  •     将新的值赋值给 value
[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操作对应实际的Computer指令其实不外乎:
 

  •     Thread 1 : 读取 value, 得到 0, 加 1, 因此 value = 1
  •     Thread 2 : 读取 value, 得到 0, 加 1, 因此 value = 1
  •     Thread 1 : 将 1 赋值给 value,然后回到 1
  •     Thread 2 : 将 1 赋值给 value,然后回到 1
movl  counter(%rip), %eax
addl  $1, %eax
movl  %eax, counter(%rip)

这种气象大家誉为多线程的交错推行,也正是说八线程也许在同二个时光点实施相似的讲话,就算独有八个线程,交错的面貌也很精通。倘若你有更加多的线程、越来越多的操作供给施行,那么这一个交错是必然产生的。

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

有无数措施来消除线程交错的题目:

自己听到你在交头接耳:这无庸置疑,可为啥会促成数数不当的标题啊?嗯,还记得大家早前说过线程会分享管理器,因为唯有单核。因而在一些点上,一个线程会固守指令施行到位,但在无尽场所下,操作系统会对线程说:时间甘休了,到前面排队再来,然后其它二个线程领头实行,当下二个线程开首实行时,它会从被搁浅的不得了地方上马施行。所以您猜会发生如何事,当前线程正图谋实施寄存器加1操作时,系统把计算机交给此外三个线程?

  •     信号量 Semaphores
  •     原子引用 Atomic references
  •     Monitors
  •     Condition codes
  •     Compare and swap

自身实在不清楚会产生什么样事,只怕大家在打算加1时,其余二个线程进来了,重新将
counter 值加载到寄放器等多样场所包车型大巴爆发。何人也不亮堂毕竟爆发了怎么。

在这里篇文章中我们将学习怎么样使用频限信号量来解决这一个标题。时域信号量也许有众多个人名为互斥量(Mutex),同叁个时光只同意三个线程获取三个排斥对象的锁,通过
Mutex 的简便属性就可以用来缓和交错的难题。

科学的做法

动用 Mutex 让计数器程序是线程安全的

解决方案正是供给同贰个时间内只同意一个线程访问分享变量。那几个可通过
std::mutex
类来缓慢解决。当线程步入时,加锁、实施操作,然后释放锁。其余线程想要访谈那一个分享财富必须等待锁释放。

在 C++11 线程库中,互斥量富含在 mutex 头文件中,对应的类是
std::mutex,有多个至关心保护要的秘籍 mutex:lock() 和 unlock()
,从名字上可查出是用来锁对象以至释放锁对象。生龙活虎旦有个别互斥量被锁,那么再一次调用
lock() 重回堵塞值得该对象被保释。

互斥(mutex)
是操作系统确定保证锁和平解决锁操作是不可分割的。那意味着线程在对互斥量进行锁和平解决锁的操作是不会被中断的。当线程对互斥量举办锁还是解锁时,该操作会在操作系统切换线程前实现。

为了让大家刚刚的计数器结构体是线程安全的,我们增添一个 set:mutext
成员,并在各类方法中经过 lock()/unlock() 方法来拓宽保险:
 

而最佳的职业是,当您希图对互斥量实行加锁操作时,别的的线程已经锁住了该互斥量,那您就亟须等待直到其释放。操作系统会追踪哪个线程正在等候哪个互斥量,被堵塞的线程会进来
“blocked onm”
状态,意味着操作系统不会给那几个堵塞的线程任哪管理器时间,直到互斥量解锁,由此也不会浪费
CPU
的循环。若是有多少个线程处于等候状态,哪个线程最初获得财富决计于操作系统本身,日常像
Windows 和 Linux 系统应用的是 FIFO
攻略,在实时操作系统中则是基于优先级的。

struct Counter {
  std::mutex mutex;
  int value;

  Counter() : value(0) {}

  void increment(){
    mutex.lock();
    ++value;
    mutex.unlock();
  }
};

目前让我们对地点的代码举办改进:
 

接下来大家再度测验这么些顺序,打字与印刷的结果正是 500 了,而且每一回都如出意气风发辙。

#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操作,然后释放互斥量。

近些日子让大家来看其余风姿罗曼蒂克种景况,想象我们的的计数器有一个减操作,并在值为0的时候抛出特别:
 

再次实践上述顺序,结果如下:
 

struct Counter {
  int value;

  Counter() : value(0) {}

  void increment(){
    ++value;
  }

  void decrement(){
    if(value == 0){
      throw "Value cannot be less than 0";
    }

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

接下来大家不必要改正类来拜望这些结构体,我们创立五个封装器:
 

那下数对了。不过在计算机科学中,没有无需付费的中饭,使用互斥量会稳中有降程序的脾性,但那总比贰个漏洞百出的次序要强吧。

struct ConcurrentCounter {
  std::mutex mutex;
  Counter counter;

  void increment(){
    mutex.lock();
    counter.increment();
    mutex.unlock();
  }

  void decrement(){
    mutex.lock();
    counter.decrement();    
    mutex.unlock();
  }
};

卫戍万分

比比较多时候该封装器运行相当好,可是选用 decrement
方法的时候就能有非常爆发。那是二个大主题材料,意气风发旦这些发生后,unlock
方法就没被调用,导致互斥量一直被占用,然后全数程序就直接处于堵塞情形(死锁),为了消除那么些题目大家须要用
try/catch 结构来拍卖格外情状:
 

当对变量进行加1操作时,是恐怕会时有产生特别的,当然在大家以那件事例中发出十分的机缘一丁点儿,不过在一些繁缛系统中是极有望的。上边的代码并非非凡安全的,当相当产生时,程序已经截至了,可是互斥量仍处锁的意况。

void decrement(){
  mutex.lock();
  try {
    counter.decrement();
  } catch (std::string e){
    mutex.unlock();
    throw e;
  }
  mutex.unlock();
}

为了有限帮助互斥量在老大发生的情形下也能被解锁,大家必要采取如下代码:
 

其一代码并轻巧,但看起来超级丑,要是你四个函数有 十个退出点,你就非得为每个退出点调用贰次 unlock
方法,大概你大概在某些地点忘掉了 unlock
,那么种种正剧就要发生,喜剧产生将直接产生程序死锁。

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
对象,然后在得了时释放。

电动锁处理

继续改进代码:
 

当您需求满含整段的代码(在大家这里是三个艺术,也说不定是四个循环体只怕其余的调节结构),有这样大器晚成种好的解决办法能够制止忘记释放锁,那就是std::lock_guard.

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
  }
}

其意气风发类是贰个粗略的智能乌棒理器,但创立 std::lock_guard
时,会自动调用互斥量对象的 lock() 方法,当 lock_guard
析构时会自动释放锁,请看上边代码:

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

 

铭记,请使用放下代码模板来编排:

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> guar(mutex);
    mutex.unlock();
  }
};

 

是或不是看起来爽多了?

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
  }
}

使用 lock_guard ,你不再须要思索怎么着时候要释放锁,这几个职业已经由
std::lock_guard 实例帮您做到。

您可能感兴趣的篇章:

  • ASP.NET Core 数据爱抚(Data
    Protection)上篇
  • 在ASP.NET
    2.0中操作数据之七十生龙活虎:体贴连接字符串及别的设置消息
  • Oracle数据库
    DGbroker二种爱抚方式的切换
  • 研讨在C++程序并发时爱惜分享数据的标题
  • 使用DOS命令来对抗U盘病毒保护U盘数据
  • 选用MySQL加密函数保养Web网站敏感数据的法子分享
  • 设置密码珍视的SqlServer数据库备份文件与回复文件的主意
  • 怎样有限支持MySQL中至关心尊敬要数据的措施
  • 护卫你的Sqlite数据库(SQLite数据库安全秘诀)
  • ASP.NET Core 数据珍贵(Data Protection
    集群场景)下篇

结论

在这里篇文章中大家上学了什么通过时限信号量/互斥量来保障分享数据。必要记住的是,使用锁会减少程序品质。在一些高并发的应用境况中有其余越来越好的化解办法,可是那不在本文的研商范畴之内。

您能够在 Github 上收获本文的源码.

您只怕感兴趣的稿子:

  • ASP.NET Core 数据爱戴(Data
    Protection)上篇
  • 在ASP.NET
    2.0中操作数据之八十风流倜傥:珍惜连接字符串及其余设置音信
  • Oracle数据库
    DGbroker三种珍惜方式的切换
  • C++八线程编制程序时的数据爱护
  • 行使DOS命令来对抗U盘病毒爱抚U盘数据
  • 使用MySQL加密函数爱护Web网址敏感数据的法子分享
  • 安装密码爱护的SqlServer数据库备份文件与还最初的作品件的主意
  • 什么样保证MySQL中至关心注重要数据的艺术
  • 护卫你的Sqlite数据库(SQLite数据库安全法门)
  • ASP.NET Core 数据爱慕(Data Protection
    集群场景)下篇

Post Author: admin

发表评论

电子邮件地址不会被公开。 必填项已用*标注