目录
一.线程间数据共享的问题
问题:你和你的朋友合租一个公寓, 公寓中只有一个厨房和一个卫生间。 当你的朋友在卫生间时, 你就会不能使用了;或者在购票网站上两个人订票,同一个座位,当一行订票操作时,另一个人就不能再操作或者即使操作也不会成功,否则就会出很多的麻烦。
同样的问题,对于多线程来说,有以下几种情况:
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::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成员函数
- lock()
std::unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock(); //自动unlock()
- unlock()
std::unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
//...处理共享数据
myUniLock.unlock(); //暂时解开
//...处理非共享数据
myUniLock.lock();
- try_lock()
std::unique_lock<std::mutex> guard(my_mutex,std::defer_lock);
if(guard.try_lock() == true)
{
//拿到了锁
dataList.push_back(i);
}
else
{
//没拿到锁
cout<<"没拿到锁"<<endl;
}
- release()
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)能够保护较多的数据量。要选择合适粒度的锁。
七.unique_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();