程序并发时保护共享数据的问题

 大家先通过三个简练的代码来打听该难点。
一齐难点

文化链接:

大家使用二个归纳的构造体
Counter,该结构体包涵二个值以致一个办法用来更动这几个值:
 

C++11并发之std::thread

struct Counter {
  int value;

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

 

然后运营七个线程来更正结构体的值:

本文概要:

 

1、 头文件。

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

2、std::mutex。

咱俩运营了5个线程来扩大计数器的值,种种线程增添了玖17遍,然后在线程停止时打字与印刷计数器的值。

3、std::recursive_mutex。

但大家运营那几个程序的时候,我们是期望它会承诺500,但实际情状不是这样,没人能方便知道程序将打字与印刷什么结果,上边是在本身机器上运营后打字与印刷的数码,何况每一次都不可比量齐观:
 

4、std::time_mutex。

442
500
477
400
422
487

5、std::lock_guard 与 std::unique_lock。

主题材料的原故在于更换计数器值并非二个原子操作,供给经过下边多少个操作技巧做到三次计数器的加码:

 

  •     首先读取 value 的值
  •     然后将 value 值加1
  •     将新的值赋值给 value

Mutex 又称互斥量,C++ 1第11中学与 Mutex 相关的类(包罗锁类型)和函数都声称在
#include 头文件中,所以只要您需求利用 std::mutex,就必得含有 #include
头文件。

但您使用单线程来运行那个程序的时候自然未有其余难题,由以前后相继是逐黄金年代推行的,但在多线程情形中就有麻烦了,想象下上边那一个推行顺序:

 

  •     Thread 1 : 读取 value, 得到 0, 加 1, 因此 value = 1
  •     Thread 2 : 读取 value, 得到 0, 加 1, 因此 value = 1
  •     Thread 1 : 将 1 赋值给 value,然后回到 1
  •     Thread 2 : 将 1 赋值给 value,然后回来 1

1、 头文件。

这种处境大家称为多线程的交错推行,也便是说八线程也许在同四个小时点奉行形似的口舌,即使独有多少个线程,交错的景色也很明显。假设你有越多的线程、更加多的操作供给推行,那么这一个交错是明确发生的。

Mutex 系列类(四种)

有数不清艺术来消除线程交错的难题:

  • std::mutex,最宗旨的 Mutex 类。
  • std::recursive_mutex,递归 Mutex 类。
  • std::time_mutex,定时 Mutex 类。
  • std::recursive_timed_mutex,定期递归 Mutex 类。
  •     信号量 Semaphores
  •     原子援引 Atomic references
  •     Monitors
  •     Condition codes
  •     Compare and swap

Lock 类(两种)

在这里篇小说中大家将学习怎么使用时域信号量来解决这些主题素材。实信号量也是有诸四人称作互斥量(Mutex),同二个时光只允许二个线程获取三个排挤对象的锁,通过
Mutex 的简练属性就可以用来解决交错的标题。

  • std::lock_guard,与 Mutex RAII 相关,方便线程对互斥量上锁。
  • std::unique_lock,与 Mutex RAII
    相关,方便线程对互斥量上锁,但提供了越来越好的上锁息争锁调节。

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

别的类型

在 C++11 线程库中,互斥量富含在 mutex 头文件中,对应的类是
std::mutex,有两个主要的法子 mutex:lock() 和 unlock()
,从名字上可得到消息是用来锁对象以致释放锁对象。意气风发旦有个别互斥量被锁,那么再度调用
lock() 重返堵塞值得该对象被放走。

  • std::once_flag
  • std::adopt_lock_t
  • std::defer_lock_t
  • std::try_to_lock_t

为了让我们刚刚的计数器结构体是线程安全的,大家抬高三个 set:mutext
成员,并在每种方法中经过 lock()/unlock() 方法来拓宽维护:
 

函数

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

  Counter() : value(0) {}

  void increment(){
    mutex.lock();
    ++value;
    mutex.unlock();
  }
};
  • std::try_lock,尝试同时对多个互斥量上锁。
  • std::lock,能够何况对四个互斥量上锁。
  • std::call_once,假如八个线程要求同不常候调用有些函数,call_once
    能够确认保障八个线程对该函数只调用三回。

下一场大家再一次测验那几个顺序,打印的结果正是 500 了,并且每一回都同样。

 

丰硕和锁

2、std::mutex。

当今让我们来看其它大器晚成种情景,想象大家的的计数器有多少个减操作,并在值为0的时候抛出非常:
 

下边以 std::mutex 为例介绍 C++11 中的互斥量用法。

struct Counter {
  int value;

  Counter() : value(0) {}

  void increment(){
    ++value;
  }

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

    --value;
  }
};

std::mutex 是C++11 中最宗旨的互斥量,std::mutex
对象提供了侵夺全部权的特色——即不援助递归地对 std::mutex 对象上锁,而
std::recursive_lock 则能够递归地对互斥量对象上锁。

下一场大家无需改过类来拜访那么些结构体,大家创造叁个封装器:
 

冠亚体育手机网站,std::mutex 的分子函数

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

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

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

(1)构造函数,std::mutex不一致敬拷贝构造,也区别意 move 拷贝,最早发生的
mutex 对象是居于 unlocked 状态的。

大大多时候该封装器运转相当好,可是使用 decrement
方法的时候就能够有万分爆发。那是三个大难点,生龙活虎旦那多少个产生后,unlock
方法就没被调用,导致互斥量一向被挤占,然后一切程序就径直处于堵塞情形(死锁),为了缓和那么些主题素材我们须求用
try/catch 结构来管理万分情状:
 

(2)lock(),调用线程将锁住该互斥量。线程调用该函数会爆发下边 3 种情状:

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

     a)假诺该互斥量当前一直不被锁住,则调用线程将该互斥量锁住,直到调用
unlock以前,该线程一向持有该锁。

其一代码并简单,但看起来非常难看,假诺您贰个函数有 12个退出点,你就必须要为各样退出点调用贰次 unlock
方法,也许你也许在有些地点忘掉了 unlock
,那么种种正剧就要产生,喜剧发生将向来产生程序死锁。

     b)假使当前互斥量被其余线程锁住,则当前的调用线程被阻塞住。

接下去大家看怎么缓慢解决这几个难点。

     c)假若当前互斥量被当下调用线程锁住,则会时有发生死锁 (deadlock) 。

电动生鱼理

(3)unlock(),解锁,释放对互斥量的全数权。

当您要求包涵整段的代码(在大家这里是二个艺术,也说不定是贰个循环体或许其余的调控结构),有这么后生可畏种好的解决办法能够制止忘记释放锁,这正是std::lock_guard.

(4)try_lock(),尝试锁住互斥量,假设互斥量被其余线程据有,则当前线程也不会被堵塞。线程调用该函数也会产出下面3 种情况:

这些类是二个简练的智能柔鱼理器,但成立 std::lock_guard
时,会活动调用互斥量对象的 lock() 方法,当 lock_guard
析构时会活动释放锁,请看下边代码:

   
 a)假设当前互斥量未有被其余线程占领,则该线程锁住互斥量,直到该线程调用
unlock 释放互斥量。

 

     b)假若当前互斥量被别的线程锁住,则当前调用线程返回false,而并不会被封堵掉。

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

     c)假如当前互斥量被当下调用线程锁住,则会爆发死锁 (deadlock) 。

是或不是看起来爽多了?

 

使用 lock_guard ,你不再要求思考如何时候要释放锁,这么些专业早就由
std::lock_guard 实例帮您做到。

std::mutex的例子如下:

结论

  1. #include //std::cout
  2. #include //std::thread
  3. #include //std::mutex
  4. #include //std::atomic
  5. using namespace std;
  6. atomic_int counter{ 0 }; //原子变量
  7. mutex g_mtx; //互斥量
  8. void fun()
  9. {
  10. for (int i = 0; i < 1000000; ++i)
  11. {
  12. if (g_mtx.try_lock()) //尝试是否可以加锁
  13. {
  14. ++counter;
  15. g_mtx.unlock(); //解锁
  16. }
  17. }
  18. }
  19. int main()
  20. {
  21. thread threads[10];
  22. for (int i = 0; i < 10; ++i)
  23. {
  24. threads[i] = thread(fun);
  25. }
  26. for (auto & th : threads)
  27. {
  28. th.join();
  29. }
  30. cout << "counter=" << counter << endl;
  31. system("pause");
  32. return 0;
  33. }
  34. 运行结果:
  35. counter=1342244

在这里篇小说中大家学习了怎样通过数字信号量/互斥量来维护分享数据。须要记住的是,使用锁会裁减程序质量。在一些高并发的应用景况中有别的越来越好的撤销办法,可是那不在本文的评论范畴之内。

从例子可以见到,拾一个线程不会爆发死锁,由于 try_lock()
,尝试锁住互斥量,假如互斥量被其余线程据有,则当前线程也不会被打断。可是如此会促成结果不精确,那也正是线程安全的题目,后边在 C++11并发之std::thread
T7 中详尽介绍了这几个难题。

您能够在 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
    集群场景)下篇

3、std::recursive_mutex。

倘诺三个线程中恐怕在实施中供给再行获得锁的场所,按常规的做法会冒出死锁。

例如:

  1. #include //std::cout
  2. #include //std::thread
  3. #include //std::mutex
  4. using namespace std;
  5. mutex g_mutex;
  6. void threadfun1()
  7. {
  8. cout << "enter threadfun1" << endl;
  9. lock_guard lock(g_mutex);
  10. cout << "execute threadfun1" << endl;
  11. }
  12. void threadfun2()
  13. {
  14. cout << "enter threadfun2" << endl;
  15. lock_guard lock(g_mutex);
  16. threadfun1();
  17. cout << "execute threadfun2" << endl;
  18. }
  19. int main()
  20. {
  21. threadfun2(); //死锁
  22. //Unhandled exception at 0x758BC42D in Project2.exe: Microsoft C++ exception: std::system_error at memory location 0x0015F140.
  23. return 0;
  24. }
  25. 运行结果:
  26. enter threadfun2
  27. enter threadfun1
  28. //就会产生死锁

那时就要求使用递归式互斥量 recursive_mutex
来幸免这些主题素材。recursive_mutex不会爆发上述的死锁难点,只是是扩充锁的计数,但必须保障您unlock和lock的次数相像,其余线程才大概锁那几个mutex。

例如:

  1. #include //std::cout
  2. #include //std::thread
  3. #include //std::mutex
  4. using namespace std;
  5. recursive_mutex g_rec_mutex;
  6. void threadfun1()
  7. {
  8. cout << "enter threadfun1" << endl;
  9. lock_guard lock(g_rec_mutex);
  10. cout << "execute threadfun1" << endl;
  11. }
  12. void threadfun2()
  13. {
  14. cout << "enter threadfun2" << endl;
  15. lock_guard lock(g_rec_mutex);
  16. threadfun1();
  17. cout << "execute threadfun2" << endl;
  18. }
  19. int main()
  20. {
  21. threadfun2(); //利用递归式互斥量来避免这个问题
  22. return 0;
  23. }
  24. 运行结果:
  25. enter threadfun2
  26. enter threadfun1
  27. execute threadfun1
  28. execute threadfun2

结论:

std::recursive_mutex 与 std::mutex
同样,也是大器晚成种能够被上锁的对象,可是和 std::mutex
分歧的是,std::recursive_mutex
允许同二个线程对互斥量数次上锁(即递归上锁),来收获对互斥量对象的多层全体权,std::recursive_mutex
释放互斥量时索要调用与该锁档案的次序深度类似次数的 unlock(),可掌握为 lock()
次数和 unlock() 次数相符,除此而外,std::recursive_mutex 的风味和
std::mutex 大约相符。

 

4、std::time_mutex。

std::time_mutex 比 std::mutex
多了八个分子函数,try_lock_for(),try_lock_until()。

 

try_lock_for
函数接收一个时日范围,表示在此生机勃勃段时间范围以内线程若无博得锁则被阻塞住(与
std::mutex 的 try_lock() 不同,try_lock
如果被调用时未有取得锁则直接重返false),假诺在这里时期其余线程释放了锁,则该线程可以赢得对互斥量的锁,假使超时(即在指依时期内照旧未有赢得锁),则赶回
false。

例如:

  1. #include //std::cout
  2. #include //std::thread
  3. #include //std::mutex
  4. using namespace std;
  5. std::timed_mutex g_t_mtx;
  6. void fun()
  7. {
  8. while (!g_t_mtx.try_lock_for(std::chrono::milliseconds(200)))
  9. {
  10. cout << "-";
  11. }
  12. this_thread::sleep_for(std::chrono::milliseconds(1000));
  13. cout << "*" << endl;
  14. g_t_mtx.unlock();
  15. }
  16. int main()
  17. {
  18. std::thread threads[10];
  19. for (int i = 0; i < 10; i++)
  20. {
  21. threads[i] = std::thread(fun);
  22. }
  23. for (auto & th : threads)
  24. {
  25. th.join();
  26. }
  27. return 0;
  28. }
  29. 运行结果:
  30. ------------------------------------*
  31. ----------------------------------------*
  32. -----------------------------------*
  33. ------------------------------*
  34. -------------------------*
  35. --------------------*
  36. ---------------*
  37. ----------*
  38. -----*
  39. *

try_lock_until
函数则选用三个时刻点作为参数,在指按期期点未赶到此前线程若无博得锁则被阻塞住,若是在那时候期别的线程释放了锁,则该线程能够拿走对互斥量的锁,假使超时(即在指定时期内大概未有收获锁),则赶回
false。

 

5、std::lock_guard 与 std::unique_lock。

地点介绍的方法对 mutex 的加解锁都是手动的,接下去介绍 std::lock_guard
与 std::unique_lock 对 mutex 举办活动加解锁。

例如:

  1. #include //std::cout
  2. #include //std::thread
  3. #include //std::mutex
  4. #include //std::atomic
  5. using namespace std;
  6. mutex g_mtx1;
  7. atomic_int num1{ 0 };
  8. void fun1()
  9. {
  10. for (int i = 0; i < 10000000; i++)
  11. {
  12. unique_lock ulk(g_mtx1);
  13. num1++;
  14. }
  15. }
  16. mutex g_mtx2;
  17. atomic_int num2{ 0 };
  18. void fun2()
  19. {
  20. for (int i = 0; i < 10000000; i++)
  21. {
  22. lock_guard lckg(g_mtx2);
  23. num2++;
  24. }
  25. }
  26. int main()
  27. {
  28. thread th1(fun1);
  29. thread th2(fun1);
  30. th1.join();
  31. th2.join();
  32. cout << "num1=" << num1 << endl;
  33. thread th3(fun2);
  34. thread th4(fun2);
  35. th3.join();
  36. th4.join();
  37. cout << "num2=" << num2 << endl;
  38. return 0;
  39. }
  40. 运行结果:
  41. num1=20000000
  42. num2=20000000

接下去,解析一下那五头的区分:

(1)unique_lock。

unique_lock ulk(g_mtx1);

线程没有 g_mtx1 的全数权,依照块语句的大循环达成活动加解锁。

线程依照 g_mtx1 属性,来剖断是不是足以加锁、解锁。

(2)lock_guard。

lock_guard lckg(g_mtx2);

线程具备 g_mtx2 的全部权,完毕全自动加解锁。

线程读取 g_mtx2 退步时,则直接等候,直到读取成功。

线程会把  g_mtx2 一贯并吞,直到当前线程落成才放走,别的线程才干访问。

Post Author: admin

发表评论

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