互斥锁mutex,本质是一个类对象
多个线程的执行顺序是乱的,所以会出现交叉打印,这是因为系统的调度机制,可以使用互斥锁限制线程之间的执行
lock():
多个线程尝试调用lock(),但只会有一个线程可以上锁
只保护需要保护的代码段,比如需要修改的全局变量,可读可写的数据
一个lock()可以对应多个unlock(),因为可能有多条分支,每条路都需要一个解锁
为了避免忘记写unlock(),可以使用lock_guard()自动管理,有了lock_guard就不能lock/unlock
lock_guard():
是一个模板类,原理是在其生命周期中构造时lock(),析构时unlock()
并且只有构造和析构方法,拷贝构造赋值重载移动方法已经被禁用
为了限定作用域而非整个函数,可以使用{}进行作用域限制
两个构造函数:
lock_guard(mutex &):构造加锁,析构解锁
lock_guard(mutex &, adopt_lock):假设线程已经获得互斥体所有权并进行管理。构造不加锁,析构解锁
上锁不上锁的例子1
注意:使用同一个mutex对象对同一个资源上锁,使一个时间段内只能有一个线程访问它
// 一个没有上锁的反面例子,会导致两个线程交替执行并打印字符
void print1(int cnt, char op)
{
while (cnt--)
cout << op << ' ';
cout << endl;
}
mutex mtx; // 全局对象
void print2(int cnt, char op)
{
mtx.lock(); // 表明线程执行时对资源上锁
while (cnt--)
cout << op << ' ';
cout << endl;
mtx.unlock(); // 资源解锁
}
int main()
{
//thread a(print1, 10, '*');
//thread b(print1, 10, '&'); // 没上锁,所以因为时间片或什么原因导致交替执行线程
thread a(print2, 10, '*');
thread b(print2, 10, '&'); // 因为调用函数时上锁了,所以会按join顺序执行
a.join(), b.join();
return 0;
}
上锁不上锁的例子2
使用不同mutex对象访问不同资源,也会导致交替打印,甚至另一个线程还可以修改全局变量
因为不同mutex对象管理不同函数资源,每个线程执行时不冲突,所以加锁跟没加似的
但是如果线程都访问同一个函数,就是使用同一个mutex对象控制同一个函数资源,所以上锁成功
int gNum = 1;
mutex mtx1, mtx2;
void print1(int cnt, char op)
{
mtx1.lock();
for (int i = 0; i < cnt; i++)
{
gNum = 1;
cout << gNum << ' ';
}
cout << endl;
mtx1.unlock();
}
void print2(int cnt, char op)
{
mtx2.lock();
gNum = 2;
for (int i = 0; i < cnt; i++)
{
gNum = 2;
cout << gNum << ' ';
}
cout << endl;
mtx2.unlock();
}
int main()
{
thread a(print1, 10, '*'); // 此时会交叉打印,因为线程之间的锁没有关联
thread b(print2, 10, '&'); // 分别试试都调用print1或print2,不会交替打印
a.join(), b.join();
return 0;
}
使用lock_guard的例子
mutex mtx;
void print1(int num) // 使用lock()和unlock()上锁
{
mtx.lock();
cout << "当前线程号:" << num << endl; // 注意不是一定顺序的,看系统调度
mtx.unlock();
}
void print2(int num) // 使用lock_guard()管理锁
{
{ // 注意这个{}是为了限制lock_guard()的作用域,使其在这段{}当中而非整个print2()
lock_guard<mutex> lg(mtx); // 使用互斥锁对象给lg赋初值
cout << "当前线程号:" << num << endl;
}
}
int main()
{
vector<thread> threads;
for (int i = 0; i < 10; i++) // 插入的同时也开始执行线程(临时对象写法)
//threads.push_back(thread(print1, i));
threads.push_back(thread(print2, i)); // 只要我new了线程,就会受调度影响导致乱序
for (auto iter = threads.begin(); iter != threads.end(); ++iter)
iter->join();
cout << endl << "下面执行主线程:" << endl;
return 0;
}
死锁
至少有两个互斥锁才会导致死锁问题。程序中有两个线程都需要使用mtx1和mtx2进行上锁
但是前者先mtx1再mtx2,后者先mtx2再mtx1,就会出现一种交叉加锁的现象
此时就会发生死锁,解锁的次序无所谓
死锁的一般解决方法:
1:使二者都是先mtx1再mtx2,而不是交叉上锁
lock_guard也是同理,需要保证两个线程中上锁的顺序都是一致的
2:使用std::lock()函数模板
std::lock():
一次可以锁住两个及以上的互斥量,一个mutex不行
不会出现死锁问题,因为lock()要求只有当所有互斥量都锁住了才会继续执行
如果有一个互斥量上锁失败就会释放其他所有锁防止死锁发生
要么一口气都锁住,要么都没锁住等待时机,不存在一部分锁一部分无锁的情况发生
用法:lock(mtx1,mtx2,mtx3...);
为了避免std::lock()后忘记mtx.unlock()
可以利用lock_guard管理unlock,需要再加一个参数adopt_lock
注意lock_guard默认构造加锁,析构解锁,但由于已经使用std::lock()加了锁
所以使用adopt_lock结构体标记不需要再次进行lock()了,只负责unlock()就好
代码示意
int main()
{
mutex mtx1, mtx2, mtx3, mtx4;
// 1 使用std::lock()+unlock()管理多个互斥量的加解锁
lock(mtx1, mtx2);
mtx1.unlock(), mtx2.unlock();
// 2 使用lock_guard+adopt_lock自动管理unlock()
lock(mtx3, mtx4);
lock_guard<mutex> lg3(mtx3, adopt_lock), lg4(mtx4, adopt_lock);
return 0;
}