1.mutex
头文件 #include <mutex>
mutex 是一个类,其源码如下图所示。
互斥锁其实就是一个 mutex 类对象,多个线程尝试用成员函数 lock() 来加锁,只有一个线程能锁定成功,成功的标志是 lock() 函数返回,如果没有锁成功,那么流程将卡在 lock() 这里不断尝试去锁定。
lock() 和 unlock() 要成对使用,每调用一次 lock(),必然要调用一次 unlock(),非对称数量的调用都会导致代码的不稳定甚至崩溃。
注意:如果程序有多个出口,那么每个出口都要调用一次 unlock()。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
int ticketCount = 100; // 一共有100张车票,由三个窗口一起卖票
std::mutex mtx; // 全局的一把互斥锁
// 模拟卖票的线程函数
void sellTicket(int index)
{
while (ticketCount > 0)
{
mtx.lock();
cout << index << " => " << ticketCount << endl;
ticketCount--;
mtx.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
list<std::thread> threadList;
for (int i = 1; i <= 3; ++i)
{
threadList.push_back(std::thread(sellTicket, i));
}
for (std::thread& t : threadList)
{
t.join();
}
cout << "所有窗口卖票结束!" << endl;
return 0;
}
上面的代码存在问题,当 ticketCount
为
1
1
1 时,会出现下图的结果,可以使用锁+双重判断来解决。
2.锁+双重判断
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
int ticketCount = 100; // 一共有100张车票,由三个窗口一起卖票
std::mutex mtx; // 全局的一把互斥锁
// 模拟卖票的线程函数
void sellTicket(int index)
{
while (ticketCount > 0)
{
mtx.lock();
// 临界区代码段
if (ticketCount > 0) // 双重判断
{
cout << index << " => " << ticketCount << endl;
ticketCount--;
}
mtx.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
list<std::thread> threadList;
for (int i = 1; i <= 3; ++i)
{
threadList.push_back(std::thread(sellTicket, i));
}
for (std::thread& t : threadList)
{
t.join();
}
cout << "所有窗口卖票结束!" << endl;
return 0;
}
3.lock_guard
为了防止大家忘记 unlock(),引入了一个叫做 std::lock_guard 的类模板,你忘记 unlock() 不要紧,它替你 unlock(),其源码如下图所示。
3.1 lock_guard的构造函数
在构造函数中,执行了 lock()。
3.2 lock_guard的析构函数
在离开作用域时,调用析构函数,执行了 unlock()。
3.3 lock_guard禁止拷贝构造和拷贝赋值
lock_guard 禁止拷贝构造和拷贝赋值,也就是说,lock_guard 不能用在函数参数传递或返回过程中,只能用在简单的临界区代码段的互斥操作中。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
int ticketCount = 100; // 一共有100张车票,由三个窗口一起卖票
std::mutex mtx; // 全局的一把互斥锁
// 模拟卖票的线程函数
void sellTicket(int index)
{
while (ticketCount > 0)
{
{
lock_guard<std::mutex> mylock(mtx);
// 临界区代码段
if (ticketCount > 0)
{
cout << index << " => " << ticketCount << endl;
ticketCount--;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
list<std::thread> threadList;
for (int i = 1; i <= 3; ++i)
{
threadList.push_back(std::thread(sellTicket, i));
}
for (std::thread& t : threadList)
{
t.join();
}
cout << "所有窗口卖票结束!" << endl;
return 0;
}
4.unique_lock
unique_lock 也是一个类模板,与 lock_guard 相比,它有自己的参数和成员函数可以更加灵活地进行锁的操作。
4.1 unique_lock的析构函数
在离开作用域时,调用析构函数,执行了 unlock()。
4.2 unique_lock的构造函数
4.2.1 没有第二个参数
如果构造函数没有第二个参数,则默认执行 lock(),此时 unique_lock 和 lock_guard 没有什么区别,都是通过析构函数来 unlock() 的。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
int ticketCount = 100; // 一共有100张车票,由三个窗口一起卖票
std::mutex mtx; // 全局的一把互斥锁
// 模拟卖票的线程函数
void sellTicket(int index)
{
while (ticketCount > 0)
{
{
unique_lock<std::mutex> mylock(mtx);
// 临界区代码段
if (ticketCount > 0)
{
cout << index << " => " << ticketCount << endl;
ticketCount--;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
list<std::thread> threadList;
for (int i = 1; i <= 3; ++i)
{
threadList.push_back(std::thread(sellTicket, i));
}
for (std::thread& t : threadList)
{
t.join();
}
cout << "所有窗口卖票结束!" << endl;
return 0;
}
4.2.2 第二个参数是std::adopt_lock
如果构造函数的第二个参数是 std::adopt_lock
,表示这个互斥量已经被 lock() 了,不需要再重复 lock(),即“假设互斥量已经 lock() 成功,不需要再在构造函数中 lock() 这个互斥量了”。使用 std::adopt_lock 参数的前提是这个互斥量之前必须被 lock() 过。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
int ticketCount = 100; // 一共有100张车票,由三个窗口一起卖票
std::mutex mtx; // 全局的一把互斥锁
// 模拟卖票的线程函数
void sellTicket(int index)
{
while (ticketCount > 0)
{
{
mtx.lock();
unique_lock<std::mutex> mylock(mtx, std::adopt_lock);
// 临界区代码段
if (ticketCount > 0)
{
cout << index << " => " << ticketCount << endl;
ticketCount--;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
list<std::thread> threadList;
for (int i = 1; i <= 3; ++i)
{
threadList.push_back(std::thread(sellTicket, i));
}
for (std::thread& t : threadList)
{
t.join();
}
cout << "所有窗口卖票结束!" << endl;
return 0;
}
4.2.3 第二个参数是std::try_to_lock
如果构造函数的第二个参数是 std::try_to_lock
,则会尝试用 lock() 去锁定这个互斥量,但如果没有锁定成功,也会立即返回,并不会阻塞在那里。也就是说,会判断当前互斥量能否被 lock(),如果不能被 lock(),可以先去执行其他代码,避免一些不必要的等待。使用 std::try_to_lock 参数的前提是这个互斥量不能提前加锁。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
int ticketCount = 100; // 一共有100张车票,由三个窗口一起卖票
std::mutex mtx; // 全局的一把互斥锁
// 模拟卖票的线程函数
void sellTicket(int index)
{
while (ticketCount > 0)
{
{
unique_lock<std::mutex> mylock(mtx, std::try_to_lock);
if (mylock.owns_lock() == true)
{
// 如果加锁成功,则执行临界区代码段
if (ticketCount > 0)
{
cout << index << " => " << ticketCount << endl;
ticketCount--;
}
}
else
{
// 如果加锁失败,则执行一些不是临界区的其它代码
cout << "lock fail" << endl;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
list<std::thread> threadList;
for (int i = 1; i <= 3; ++i)
{
threadList.push_back(std::thread(sellTicket, i));
}
for (std::thread& t : threadList)
{
t.join();
}
cout << "所有窗口卖票结束!" << endl;
return 0;
}
4.2.4 第二个参数是std::defer_lock
如果构造函数的第二个参数是 std::defer_lock
,表示暂时先不 lock(),之后手动去 lock(),即初始化了一个没有加锁的互斥量。使用 std::defer_lock 参数的前提是这个互斥量不能提前加锁。
std::defer_lock 参数一般和 unique_lock 的成员函数搭配起来使用。
4.3 unique_lock的成员函数
4.3.1 lock()和unlock()
当使用了 std::defer_lock 参数时,在创建 unique_lock 对象时就不会自动加锁了,此时就需要借助成员函数 lock() 来进行手动加锁,当然也有 unlock() 来手动解锁。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
int ticketCount = 100; // 一共有100张车票,由三个窗口一起卖票
std::mutex mtx; // 全局的一把互斥锁
// 模拟卖票的线程函数
void sellTicket(int index)
{
while (ticketCount > 0)
{
unique_lock<std::mutex> mylock(mtx, std::defer_lock); // mtx没有加锁
mylock.lock();
// 如果加锁成功,则执行临界区代码段
if (ticketCount > 0)
{
cout << index << " => " << ticketCount << endl;
ticketCount--;
}
mylock.unlock();
// 如果没有加锁,则执行一些不是临界区的代码
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
list<std::thread> threadList;
for (int i = 1; i <= 3; ++i)
{
threadList.push_back(std::thread(sellTicket, i));
}
for (std::thread& t : threadList)
{
t.join();
}
cout << "所有窗口卖票结束!" << endl;
return 0;
}
4.3.2 try_lock()
当使用了 std::defer_lock 参数时,在创建 unique_lock 对象时就不会自动加锁了,此时可以调用 try_lock() 函数尝试给互斥量加锁,如果拿到了锁,返回 true;如果拿不到锁,返回 false。try_lock() 函数是不阻塞的。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
int ticketCount = 100; // 一共有100张车票,由三个窗口一起卖票
std::mutex mtx; // 全局的一把互斥锁
// 模拟卖票的线程函数
void sellTicket(int index)
{
while (ticketCount > 0)
{
{
unique_lock<std::mutex> mylock(mtx, std::defer_lock); // mtx没有加锁
if (mylock.try_lock() == true)
{
// 如果加锁成功,则执行临界区代码段
if (ticketCount > 0)
{
cout << index << " => " << ticketCount << endl;
ticketCount--;
}
}
else
{
// 如果加锁失败,则执行一些不是临界区的代码
cout << "lock fail" << endl;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
list<std::thread> threadList;
for (int i = 1; i <= 3; ++i)
{
threadList.push_back(std::thread(sellTicket, i));
}
for (std::thread& t : threadList)
{
t.join();
}
cout << "所有窗口卖票结束!" << endl;
return 0;
}
4.3.3 release()
release() 函数:解除 unique_lock 和 mutex 对象的联系,并将原 mutex 对象的指针返回出来。如果 mutex 对象之前已经加锁了,那么后面需要自己手动 unlock() 解锁。
#include <iostream>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
int ticketCount = 100; // 一共有100张车票,由三个窗口一起卖票
std::mutex mtx; // 全局的一把互斥锁
// 模拟卖票的线程函数
void sellTicket(int index)
{
while (ticketCount > 0)
{
unique_lock<std::mutex> mylock(mtx); // mtx自动加锁
std::mutex* pmtx = mylock.release();
// 临界区代码段
if (ticketCount > 0)
{
cout << index << " => " << ticketCount << endl;
ticketCount--;
}
pmtx->unlock(); // 自己负责手动unlock()解锁
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main()
{
list<std::thread> threadList;
for (int i = 1; i <= 3; ++i)
{
threadList.push_back(std::thread(sellTicket, i));
}
for (std::thread& t : threadList)
{
t.join();
}
cout << "所有窗口卖票结束!" << endl;
return 0;
}
4.4 unique_lock禁止拷贝构造和拷贝赋值
4.5 unique_lock允许移动构造和移动赋值
与 lock_guard 不同,unique_lock 不仅可以用在简单的临界区代码段的互斥操作中,还能用在函数参数传递或返回过程中。