赞
踩
目录
问题:你和你的朋友合租一个公寓, 公寓中只有一个厨房和一个卫生间。 当你的朋友在卫生间时, 你就会不能使用了;或者在购票网站上两个人订票,同一个座位,当一行订票操作时,另一个人就不能再操作或者即使操作也不会成功,否则就会出很多的麻烦。
同样的问题,对于多线程来说,有以下几种情况:
1.当两个线程访问不同的内存位置时,不会存在问题,相当于你和朋友不合租,各住各的;
2.两个线程对共享数据进行操作时,如果只是一起读取,不会出什么问题;
3.但是一个线程要读取,另一个线程要写入,就会出现问题,所以保护共享数据是需要在多线程中考虑的。
以下代码未考虑读数据线程和写数据线程之间的数据共享问题,会导致刚刚分析过的问题而报错。
- #include <iostream>
- #include <thread>
- #include <list>
- using namespace std;
-
- class OperateData{
- public:
- //写入数据
- void writeData()
- {
- for (int i = 0; i < 10000; i++)
- {
- cout << "写入数据" << i << endl;
- dataList.push_back(i);
- }
- }
- //读取数据
- void readData()
- {
- for (int i = 0; i < 10000; i++)
- {
- if (!dataList.empty())//数据不为空
- {
- int data = dataList.front();//返回第一个元素,但不检查元素是否存在
- dataList.pop_front();//移除第一个元素,但不返回
- }
- else
- {
- //数据为空
- cout << "数据为空"<<endl;
- }
- }
- cout << "end" << endl;
- }
- private:
- list<int> dataList;
- };
- int main()
- {
- OperateData myobj;
- thread readObj(&OperateData::readData,&myobj);//第二个参数是引用才能保证线程里用的是同一个对象
- thread writeObj(&OperateData::writeData, &myobj);
- readObj.join();
- writeObj.join();
- }
使用互斥量(mutex)保护共享数据
当访问共享数据前, 将数据锁住, 在访问结束后, 再将数据解锁。 线程库需要保证, 当一个线程使用特定互斥量锁住共享数据时, 其他的线程想要访问锁住的数据, 都必须等到之前那个线程对数据进行解锁后, 才能进行访问。
互斥量自身也有问题, 也会造成死锁, 或对数据保护的太多(或太少)。
首先需要引入头文件:
#include <mutex>
C++中通过实例化 std::mutex 创建互斥量实例, 通过成员函数lock()对互斥量上锁, unlock()
进行解锁。也就是:
lock()-->操作共享数据-->unlock()
lock()和unlock()一定要成对使用!
- #include <iostream>
- #include <thread>
- #include <list>
- #include <mutex>
- using namespace std;
-
- class OperateData{
- public:
- //写入数据
- void writeData()
- {
- for (int i = 0; i < 100000; i++)
- {
- cout << "写入数据" << i << endl;
- my_mutex.lock();
- dataList.push_back(i);
- my_mutex.unlock();
- }
- }
- //读取数据
- void readData()
- {
- for (int i = 0; i < 100000; i++)
- {
- my_mutex.lock();
- if (!dataList.empty())//数据不为空
- {
- int data = dataList.front();//返回第一个元素,但不检查元素是否存在
- dataList.pop_front();//移除第一个元素,但不返回
- }
- else
- {
- //数据为空
- cout << "数据为空"<<endl;
- }
- my_mutex.unlock();
- }
- cout << "end" << endl;
- }
- private:
- list<int> dataList;
- mutex my_mutex;
- };
- int main()
- {
- OperateData myobj;
- thread readObj(&OperateData::readData,&myobj);//第二个参数是引用才能保证线程里用的是同一个对象
- thread writeObj(&OperateData::writeData, &myobj);
- readObj.join();
- writeObj.join();
-
- return 0;
- }
修改后的程序就可以稳定运行了。
由于成对使用时容易遗漏,C++标准库为互斥量提供了一个模板类 std::lock_guard ,在构造时就能提供已锁(执行lock)的互斥量, 并在析构的时候进行解锁(执行unlock), 从而保证了一个已锁互斥量能被正确解锁。使用了lock_guard就不能使用lock()和unlock()了。
std::lock_guard<std::mutex> guard(my_mutex);
- //写入数据
- void writeData()
- {
- for (int i = 0; i < 100000; i++)
- {
- cout << "写入数据" << i << endl;
- lock_guard<std::mutex> guard(my_mutex);
- dataList.push_back(i);
- }
- }
- //读取数据
- void readData()
- {
- for (int i = 0; i < 100000; i++)
- {
- lock_guard<std::mutex> guard(my_mutex);
- if (!dataList.empty())//数据不为空
- {
- int data = dataList.front();//返回第一个元素,但不检查元素是否存在
- dataList.pop_front();//移除第一个元素,但不返回
- }
- else
- {
- //数据为空
- cout << "数据为空"<<endl;
- }
- }
- cout << "end" << endl;
- }
一对线程需要对他们所有的互斥量做一些操作, 其中每个线程都有一个互斥量, 且等待另一个解锁。 这样没有线程能工作, 因为他们都在等待对方释放互斥量。 这种情况就是死锁, 它的最大问题就是由两个或两个以上的互斥量来锁定一个操作。
死锁产生的前提条件是至少有两个互斥量
两个线程A,B
(1)线程A执行,互斥量A锁定,接下来轮到互斥量B锁定
(2)这时恰好出现上下文切换
(3)线程B执行,互斥量B锁定,接下来轮到互斥量A锁定,然后...发现A已经锁了
(4)这时就出现了死锁
出现互锁情况的代码:
- #include <iostream>
- #include <thread>
- #include <list>
- #include <mutex>
- using namespace std;
-
- class OperateData{
- public:
- //写入数据
- void writeData()
- {
- for (int i = 0; i < 100000; i++)
- {
- cout << "写入数据" << i << endl;
- my_mutex_1.lock(); //先锁1后锁2
- my_mutex_2.lock();
- dataList.push_back(i);
- my_mutex_2.unlock();
- my_mutex_1.unlock(); //解锁的顺序无所谓
- }
- }
- //读取数据
- void readData()
- {
- for (int i = 0; i < 100000; i++)
- {
- my_mutex_2.lock(); //先锁2后锁1
- my_mutex_1.lock();
- if (!dataList.empty())//数据不为空
- {
- int data = dataList.front();//返回第一个元素,但不检查元素是否存在
- dataList.pop_front();//移除第一个元素,但不返回
- }
- else
- {
- //数据为空
- cout << "数据为空"<<endl;
- }
- my_mutex_2.unlock();
- my_mutex_1.unlock(); //解锁的顺序无所谓
- }
- cout << "end" << endl;
- }
- private:
- list<int> dataList;
- mutex my_mutex_1;
- mutex my_mutex_2;
- };
- int main()
- {
- OperateData myobj;
- thread readObj(&OperateData::readData,&myobj);//第二个参数是引用才能保证线程里用的是同一个对象
- thread writeObj(&OperateData::writeData, &myobj);
- readObj.join();
- writeObj.join();
-
- return 0;
- }
1.只要保持两个互斥量上锁的顺序一致就不会死锁,lock_guard也是一样的;
- std::lock_guard<std::mutex> guard(my_mutex_1);
-
- std::lock_guard<std::mutex> guard(my_mutex_2);
2.上述也存在死锁风险的情况,C++标准库有办法解决这个问题, std::lock() ——可以一次性锁住多个(两个以上)的互斥量, 并且没有副作用(死锁风险)。当 std::lock 成功的获取一个互斥量上的锁, 并且当其尝试从另一个互斥量上再获取锁时, 就会有异常抛出,第一个锁也会随着异常的产生而自动释放, 所以 std::lock 要么将两个锁都锁住, 要不一个都不锁。
- std::lock(my_mutex_1,my_mutex_2);
-
- //....
-
- my_mutex_1.unlock();
-
- 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()。
- std::lock(my_mutex_1,my_mutex_2);
-
- //....
-
- std::lock_guard<std::mutex> guard_1(my_mutex_1,std::adopt_lock);
-
- std::lock_guard<std::mutex> guard_2(my_mutex_2,std::adopt_lock);
5.避免嵌套锁:一个线程已获得一个锁时, 再别去获取第二个。 因为每个线程只持有一个锁, 锁上就不会产生死锁。
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<>配合使用
- std::lock(my_mutex_1,my_mutex_2);
-
- //....
-
- std::unique_lock<std::mutex> guard_1(my_mutex_1,std::adopt_lock);
-
- 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()判断是否拿到锁。
- std::unique_lock<std::mutex> guard(my_mutex,std::try_to_lock);
- if(guard.owns_lock())
- {
- //拿到了锁
- dataList.push_back(i);
- }
- else
- {
- //没拿到锁
- cout<<"没拿到锁"<<endl;
- }
3.参数std::defer_lock
初始化了一个没有加锁的mutex,不给它加锁的目的是以后可以调用unique_lock的一些方法,不能提前lock。
4.std::unique_lock成员函数
- std::unique_lock<mutex> myUniLock(myMutex, defer_lock);
- myUniLock.lock(); //自动unlock()
- std::unique_lock<mutex> myUniLock(myMutex, defer_lock);
- myUniLock.lock();
- //...处理共享数据
- myUniLock.unlock(); //暂时解开
- //...处理非共享数据
- myUniLock.lock();
- std::unique_lock<std::mutex> guard(my_mutex,std::defer_lock);
- if(guard.try_lock() == true)
- {
- //拿到了锁
- dataList.push_back(i);
- }
- else
- {
- //没拿到锁
- cout<<"没拿到锁"<<endl;
- }
std::unique_lock<mutex> myUniLock(myMutex);相当于把myMutex和myUniLock绑定在了一起,release()就是解除绑定,返回它所管理的mutex对象的指针,并释放所有权。
- std::unique_lock<std::mutex> myUniLock(my_mutex);
- std::mutex* ptx = myUniLock.release();//解绑my_mutex和myUniLock
- //...操作共享数据
- ptx->unlock(); //需要自己手动解锁
锁的粒度是用来描述通过一个锁保护着的数据量大小。 一个细粒度锁(a fine-grained lock)能够保护较小的数据量, 一个粗粒度锁(a coarse-grained lock)能够保护较多的数据量。要选择合适粒度的锁。
复制所有权是非法的
- std::unique_lock<std:mutex> myUniLock_1(mutex);
- std::unique_lock<std:mutex> myUniLock_2(myUniLock_1);
需要移动所有权
- std::unique_lock<std:mutex> myUniLock_1(mutex);
- std::unique_lock<std:mutex> myUniLock_2(std::move(myUniLock_1)); //现在myUniLock_1指向空,myUniLock_2指向mutex
- std::unique_lock<std:mutex> lk()
- {
- std::unique_lock<std:mutex> tempUniLock(mutex);
- return tempUniLock;//移动构造函数
- }
- // 然后就可以在外层调用,在guard具有对mutex的所有权
- std::unique_lock<std::mutex> guard = lk();
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。