当前位置:   article > 正文

【C++】多线程数据共享问题、互斥量、死锁及解决方法_c++中如何解决多线程对数组元素同事读写的问题

c++中如何解决多线程对数组元素同事读写的问题

目录

一.线程间数据共享的问题

二.保护共享数据的方法

三.死锁

四.死锁的解决方案

五.std::unique_lock

六.锁的粒度

七.unique_lock所有权的传递


一.线程间数据共享的问题

问题:你和你的朋友合租一个公寓, 公寓中只有一个厨房和一个卫生间。 当你的朋友在卫生间时, 你就会不能使用了;或者在购票网站上两个人订票,同一个座位,当一行订票操作时,另一个人就不能再操作或者即使操作也不会成功,否则就会出很多的麻烦。

同样的问题,对于多线程来说,有以下几种情况:

1.当两个线程访问不同的内存位置时,不会存在问题,相当于你和朋友不合租,各住各的;

2.两个线程对共享数据进行操作时,如果只是一起读取,不会出什么问题;

3.但是一个线程要读取,另一个线程要写入,就会出现问题,所以保护共享数据是需要在多线程中考虑的。

以下代码未考虑读数据线程和写数据线程之间的数据共享问题,会导致刚刚分析过的问题而报错。

  1. #include <iostream>
  2. #include <thread>
  3. #include <list>
  4. using namespace std;
  5. class OperateData{
  6. public:
  7. //写入数据
  8. void writeData()
  9. {
  10. for (int i = 0; i < 10000; i++)
  11. {
  12. cout << "写入数据" << i << endl;
  13. dataList.push_back(i);
  14. }
  15. }
  16. //读取数据
  17. void readData()
  18. {
  19. for (int i = 0; i < 10000; i++)
  20. {
  21. if (!dataList.empty())//数据不为空
  22. {
  23. int data = dataList.front();//返回第一个元素,但不检查元素是否存在
  24. dataList.pop_front();//移除第一个元素,但不返回
  25. }
  26. else
  27. {
  28. //数据为空
  29. cout << "数据为空"<<endl;
  30. }
  31. }
  32. cout << "end" << endl;
  33. }
  34. private:
  35. list<int> dataList;
  36. };
  37. int main()
  38. {
  39. OperateData myobj;
  40. thread readObj(&OperateData::readData,&myobj);//第二个参数是引用才能保证线程里用的是同一个对象
  41. thread writeObj(&OperateData::writeData, &myobj);
  42. readObj.join();
  43. writeObj.join();
  44. }

二.保护共享数据的方法

使用互斥量(mutex)保护共享数据

当访问共享数据前, 将数据锁住, 在访问结束后, 再将数据解锁。 线程库需要保证, 当一个线程使用特定互斥量锁住共享数据时, 其他的线程想要访问锁住的数据, 都必须等到之前那个线程对数据进行解锁后, 才能进行访问。

互斥量自身也有问题, 也会造成死锁, 或对数据保护的太多(或太少)。

首先需要引入头文件:

#include <mutex>

C++中通过实例化 std::mutex 创建互斥量实例, 通过成员函数lock()对互斥量上锁, unlock()
进行解锁。也就是:

lock()-->操作共享数据-->unlock()

lock()和unlock()一定要成对使用!

  1. #include <iostream>
  2. #include <thread>
  3. #include <list>
  4. #include <mutex>
  5. using namespace std;
  6. class OperateData{
  7. public:
  8. //写入数据
  9. void writeData()
  10. {
  11. for (int i = 0; i < 100000; i++)
  12. {
  13. cout << "写入数据" << i << endl;
  14. my_mutex.lock();
  15. dataList.push_back(i);
  16. my_mutex.unlock();
  17. }
  18. }
  19. //读取数据
  20. void readData()
  21. {
  22. for (int i = 0; i < 100000; i++)
  23. {
  24. my_mutex.lock();
  25. if (!dataList.empty())//数据不为空
  26. {
  27. int data = dataList.front();//返回第一个元素,但不检查元素是否存在
  28. dataList.pop_front();//移除第一个元素,但不返回
  29. }
  30. else
  31. {
  32. //数据为空
  33. cout << "数据为空"<<endl;
  34. }
  35. my_mutex.unlock();
  36. }
  37. cout << "end" << endl;
  38. }
  39. private:
  40. list<int> dataList;
  41. mutex my_mutex;
  42. };
  43. int main()
  44. {
  45. OperateData myobj;
  46. thread readObj(&OperateData::readData,&myobj);//第二个参数是引用才能保证线程里用的是同一个对象
  47. thread writeObj(&OperateData::writeData, &myobj);
  48. readObj.join();
  49. writeObj.join();
  50. return 0;
  51. }

修改后的程序就可以稳定运行了。

由于成对使用时容易遗漏,C++标准库为互斥量提供了一个模板类 std::lock_guard ,在构造时就能提供已锁(执行lock)的互斥量, 并在析构的时候进行解锁(执行unlock), 从而保证了一个已锁互斥量能被正确解锁。使用了lock_guard就不能使用lock()和unlock()了。

std::lock_guard<std::mutex> guard(my_mutex);
  1. //写入数据
  2. void writeData()
  3. {
  4. for (int i = 0; i < 100000; i++)
  5. {
  6. cout << "写入数据" << i << endl;
  7. lock_guard<std::mutex> guard(my_mutex);
  8. dataList.push_back(i);
  9. }
  10. }
  11. //读取数据
  12. void readData()
  13. {
  14. for (int i = 0; i < 100000; i++)
  15. {
  16. lock_guard<std::mutex> guard(my_mutex);
  17. if (!dataList.empty())//数据不为空
  18. {
  19. int data = dataList.front();//返回第一个元素,但不检查元素是否存在
  20. dataList.pop_front();//移除第一个元素,但不返回
  21. }
  22. else
  23. {
  24. //数据为空
  25. cout << "数据为空"<<endl;
  26. }
  27. }
  28. cout << "end" << endl;
  29. }

三.死锁

一对线程需要对他们所有的互斥量做一些操作, 其中每个线程都有一个互斥量, 且等待另一个解锁。 这样没有线程能工作, 因为他们都在等待对方释放互斥量。 这种情况就是死锁, 它的最大问题就是由两个或两个以上的互斥量来锁定一个操作。

死锁产生的前提条件是至少有两个互斥量

两个线程A,B

(1)线程A执行,互斥量A锁定,接下来轮到互斥量B锁定

(2)这时恰好出现上下文切换        

(3)线程B执行,互斥量B锁定,接下来轮到互斥量A锁定,然后...发现A已经锁了

(4)这时就出现了死锁

出现互锁情况的代码:

  1. #include <iostream>
  2. #include <thread>
  3. #include <list>
  4. #include <mutex>
  5. using namespace std;
  6. class OperateData{
  7. public:
  8. //写入数据
  9. void writeData()
  10. {
  11. for (int i = 0; i < 100000; i++)
  12. {
  13. cout << "写入数据" << i << endl;
  14. my_mutex_1.lock(); //先锁1后锁2
  15. my_mutex_2.lock();
  16. dataList.push_back(i);
  17. my_mutex_2.unlock();
  18. my_mutex_1.unlock(); //解锁的顺序无所谓
  19. }
  20. }
  21. //读取数据
  22. void readData()
  23. {
  24. for (int i = 0; i < 100000; i++)
  25. {
  26. my_mutex_2.lock(); //先锁2后锁1
  27. my_mutex_1.lock();
  28. if (!dataList.empty())//数据不为空
  29. {
  30. int data = dataList.front();//返回第一个元素,但不检查元素是否存在
  31. dataList.pop_front();//移除第一个元素,但不返回
  32. }
  33. else
  34. {
  35. //数据为空
  36. cout << "数据为空"<<endl;
  37. }
  38. my_mutex_2.unlock();
  39. my_mutex_1.unlock(); //解锁的顺序无所谓
  40. }
  41. cout << "end" << endl;
  42. }
  43. private:
  44. list<int> dataList;
  45. mutex my_mutex_1;
  46. mutex my_mutex_2;
  47. };
  48. int main()
  49. {
  50. OperateData myobj;
  51. thread readObj(&OperateData::readData,&myobj);//第二个参数是引用才能保证线程里用的是同一个对象
  52. thread writeObj(&OperateData::writeData, &myobj);
  53. readObj.join();
  54. writeObj.join();
  55. return 0;
  56. }

四.死锁的解决方案

1.只要保持两个互斥量上锁的顺序一致就不会死锁,lock_guard也是一样的;

  1. std::lock_guard<std::mutex> guard(my_mutex_1);
  2. std::lock_guard<std::mutex> guard(my_mutex_2);

2.上述也存在死锁风险的情况,C++标准库有办法解决这个问题, std::lock() ——可以一次性锁住多个(两个以上)的互斥量, 并且没有副作用(死锁风险)。当 std::lock 成功的获取一个互斥量上的锁, 并且当其尝试从另一个互斥量上再获取锁时, 就会有异常抛出,第一个锁也会随着异常的产生而自动释放, 所以 std::lock 要么将两个锁都锁住, 要不一个都不锁。

  1. std::lock(my_mutex_1,my_mutex_2);
  2. //....
  3. my_mutex_1.unlock();
  4. my_mutex_2.unlock();

3.使用std::lock()很容易忘记使用unlock(),针对这种情况,C++17对这种情况提供了支持,std::scoped_lock<> 一种新的RAII类型模板类型,与 std::lock_guard<> 的功能等价, 这个新类型能接受不定数量的互斥量类型作为模板参数,以及相应的互斥量(数量和类型)作为构造参数。 互斥量支持构造即上锁, 与 std::lock 的用法相同, 其解锁阶段是在析构中进行。 

std::scoped_lock<std::mutex,std::mutex> guard(lhs.m,rhs.m);

4.同样的,也可以使用std::lock()和std::lock_guard<>配合使用,但是std::lock_guard<>已经在构造中默认使用的了std::lock(),所以要在参数中设置std::adopt_lock避免调用构造中的lock()。

  1. std::lock(my_mutex_1,my_mutex_2);
  2. //....
  3. std::lock_guard<std::mutex> guard_1(my_mutex_1,std::adopt_lock);
  4. std::lock_guard<std::mutex> guard_2(my_mutex_2,std::adopt_lock);

5.避免嵌套锁:一个线程已获得一个锁时, 再别去获取第二个。 因为每个线程只持有一个锁, 锁上就不会产生死锁。

五.std::unique_lock

std::unique_lock与std::lock_guard<> 的功能类似,在没有参数时可以替换使用,但是std::unique_lock更灵活,可以通过更多的参数去适配,但std::unique_lock 会占用比较多的空间, 并且比 std::lock_guard 稍慢一些。

1.参数std::adopt_lock

和上述在std::lock_guard<>中使用一样,需要std::lock()和std::lock_guard<>配合使用

  1. std::lock(my_mutex_1,my_mutex_2);
  2. //....
  3. std::unique_lock<std::mutex> guard_1(my_mutex_1,std::adopt_lock);
  4. std::unique_lock<std::mutex> guard_2(my_mutex_2,std::adopt_lock);

2.参数std::try_to_lock

不能与lock同时使用,尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里;使用try_to_lock的原因是防止其他的线程锁定mutex太长时间,导致本线程一直阻塞在lock这个地方。通过owns_lock()判断是否拿到锁。

  1. std::unique_lock<std::mutex> guard(my_mutex,std::try_to_lock);
  2. if(guard.owns_lock())
  3. {
  4. //拿到了锁
  5. dataList.push_back(i);
  6. }
  7. else
  8. {
  9. //没拿到锁
  10. cout<<"没拿到锁"<<endl;
  11. }

3.参数std::defer_lock

初始化了一个没有加锁的mutex,不给它加锁的目的是以后可以调用unique_lock的一些方法,不能提前lock。

4.std::unique_lock成员函数

  • lock()
  1. std::unique_lock<mutex> myUniLock(myMutex, defer_lock);
  2. myUniLock.lock(); //自动unlock()
  • unlock()
  1. std::unique_lock<mutex> myUniLock(myMutex, defer_lock);
  2. myUniLock.lock();
  3. //...处理共享数据
  4. myUniLock.unlock(); //暂时解开
  5. //...处理非共享数据
  6. myUniLock.lock();
  • try_lock()
  1. std::unique_lock<std::mutex> guard(my_mutex,std::defer_lock);
  2. if(guard.try_lock() == true)
  3. {
  4. //拿到了锁
  5. dataList.push_back(i);
  6. }
  7. else
  8. {
  9. //没拿到锁
  10. cout<<"没拿到锁"<<endl;
  11. }
  • release()

std::unique_lock<mutex> myUniLock(myMutex);相当于把myMutex和myUniLock绑定在了一起,release()就是解除绑定,返回它所管理的mutex对象的指针,并释放所有权。

  1. std::unique_lock<std::mutex> myUniLock(my_mutex);
  2. std::mutex* ptx = myUniLock.release();//解绑my_mutex和myUniLock
  3. //...操作共享数据
  4. ptx->unlock(); //需要自己手动解锁

六.锁的粒度

锁的粒度是用来描述通过一个锁保护着的数据量大小。 一个细粒度锁(a fine-grained lock)能够保护较小的数据量, 一个粗粒度锁(a coarse-grained lock)能够保护较多的数据量。要选择合适粒度的锁。

七.unique_lock所有权的传递

复制所有权是非法的

  1. std::unique_lock<std:mutex> myUniLock_1(mutex);
  2. std::unique_lock<std:mutex> myUniLock_2(myUniLock_1);

 需要移动所有权

  1. std::unique_lock<std:mutex> myUniLock_1(mutex);
  2. std::unique_lock<std:mutex> myUniLock_2(std::move(myUniLock_1)); //现在myUniLock_1指向空,myUniLock_2指向mutex
  1. std::unique_lock<std:mutex> lk()
  2. {
  3. std::unique_lock<std:mutex> tempUniLock(mutex);
  4. return tempUniLock;//移动构造函数
  5. }
  6. // 然后就可以在外层调用,在guard具有对mutex的所有权
  7. std::unique_lock<std::mutex> guard = lk();

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/566360
推荐阅读
相关标签
  

闽ICP备14008679号