1、资源竞争
在多线程编程中,资源竞争是一种常见的问题,指多个线程同时访问或修改共享资源,而没有适当的同步机制。资源竞争可能导致不确定的行为、数据损坏和程序崩溃。
死锁:发生死锁时,多个线程相互等待对方释放资源,导致程序无法继续执行。
2、std::mutex
互斥访问共享资源:多个线程同时读写共享变量可能导致数据不一致。使用互斥量(std::mutex)来保护共享资源,确保同一时间只有一个线程访问共享资源。
- 2.1 第一种加锁方式
/*
No.1 第一种加锁方式
mutex类(互斥量) 创建mutex类的对象
1.1 通过调用lock函数进行加锁
1.2 通过调用unlock进行解锁
注意点: lock必须与unlock成对出现
*/
class SeaKing
{
public:
void makeFriend()
{
for (int i = 0; i < 10000; i++)
{
m_mutex.lock(); //加锁
printf("增加一个女朋友:%d", i);
mm.push_back(i);
m_mutex.unlock(); //解锁
}
}
void breakUp()
{
for (int i = 0; i < 10000; i++)
{
if (!mm.empty())
{
m_mutex.lock(); //加锁
printf("分手一个女朋友:%d\n", mm.back());
mm.pop_back();
m_mutex.unlock(); //解锁
}
else
{
printf("海王变单身狗!\n");
}
}
}
protected:
list<int> mm; //共享数据
mutex m_mutex; //首先第一步构建一个互斥量对象
};
int main()
{
SeaKing man;
thread t1(&SeaKing::makeFriend, &man); //创建两个线程
thread t2(&SeaKing::breakUp, &man);
t1.join();
t2.join();
return 0;
}
不加锁的情况下,代码运行结果,程序运行不稳定
加入互斥锁后,代码运行结果,程序运行稳定
- 2.1 第二种加锁方式 std::lock_guard()
/*
No.2 第二种加锁方式
lock_guard 对象实现加锁
lock_guard类的构造函数中调用lock函数
lock_gurad类的析构函数调用了unlock函
lock_guard本质上还是调用mutex的lock()和un_lock()函数
*/
class SeaKing
{
public:
void makeFriend()
{
lock_guard<mutex> lgObject(m_mutex);
for (int i = 0; i < 10000; i++)
{
//m_mutex.lock(); //加锁
printf("增加一个女朋友:%d", i);
mm.push_back(i);
//m_mutex.unlock(); //解锁
}
}
void breakUp()
{
for (int i = 0; i < 10000; i++)
{
if (!mm.empty())
{
lock_guard<mutex> lgObject(m_mutex);
//m_mutex.lock(); //加锁
printf("分手一个女朋友:%d\n", mm.back());
mm.pop_back();
//m_mutex.unlock(); //解锁
}
else
{
printf("海王变单身狗!\n");
}
}
}
protected:
list<int> mm; //共享数据
mutex m_mutex; //首先第一步构建一个互斥量对象
};
int main()
{
SeaKing man;
thread t1(&SeaKing::makeFriend, &man);
thread t2(&SeaKing::breakUp, &man);
t1.join();
t2.join();
return 0;
}
3、std::unique_lock()
在 C++ 中,
std::unique_lock
是一个线程安全的互斥量封装类,用于管理互斥量的加锁和解锁操作。它提供了更灵活的锁定机制,并允许在不同的作用域内锁定和解锁互斥量。
- 包含头文件 ,声明一个
std::mutex
作为互斥量,并创建一个std::unique_lock
对象来管理互斥量的加锁和解锁操作。- 使用
std::unique_lock
对象来锁定互斥量。可以通过构造函数锁定互斥量,或者使用 lock() 成员函数手动锁定std::unique_lock
会在其作用域结束时自动释放互斥量的锁定。如果需要提前解锁,可以使用 unlock() 成员函数。通过使用 std::unique_lock,可以更灵活地控制互斥量的加锁和解锁操作,并在需要时进行手动解锁或重新锁定。这对于实现更复杂的锁定逻辑和避免死锁非常有用。- 需要注意的是,std::unique_lock 对象是非拷贝构造的,但可以通过移动构造。因此,通常会使用引用或指针传递 std::unique_lock 对象,以便在函数间传递和共享互斥量的锁定状态
- 除了使用默认参数之外,
std::unique_lock
还可以接受其他参数来调整其行为。以下是std::unique_lock
的一些常见参数:- 下面看一下unique_lock类的源码
3.1 std::adopt_lock
使用
std::adopt_lock
参数时,你需要确保在创建std::unique_lock
或std::lock_guard
对象之前,已经手动锁定了相应的互斥量。
这通常用于在同一线程中的不同作用域内对互斥量进行多次加锁,并确保在作用域结束时正确解锁
std::mutex mtx;
mtx.lock(); // 手动锁定互斥量,使用std::adopt_lock参数,互斥量必须先进行lock()
{
std::unique_lock<std::mutex> lock(mtx, std::adopt_lock); //使用std::adopt_lock参数
// 互斥量已经被其他方式锁定,无需再次锁定
// 在此作用域内对互斥量进行操作
// lock 在作用域结束时会自动解锁互斥量 析构函数会解锁互斥量
}
// 在此继续使用互斥量
mtx.unlock(); // 手动解锁互斥量
3.2 std::defer_lock
std::defer_lock
是std::unique_lock
的构造函数参数之一,用于指示在构造锁定对象(std::unique_lock
)时不立即锁定互斥量,而是延迟锁定操作。使用std::defer_lock 参数时,你需要手动调用
lock()` 成员函数来显式地锁定互斥量。这通常用于在构造锁定对象后的特定位置或条件下,根据需要选择性地锁定互斥量。
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 互斥量没有被锁定
// 其他操作
lock.lock(); // 手动锁定互斥量
// 在此作用域内对互斥量进行操作
// lock 在作用域结束时会自动解锁互斥量
- 使用了
std::defer_lock
参数,这样互斥量 mtx 并没有被锁定。然后,根据需要,在特定位置调用lock()
成员函数手动锁定互斥量。在lock() 调用之后的作用域内,我们可以安全地对互斥量进行操作,直到在作用域结束时lock
对象会自动解锁互斥量。 - 使用
std::defer_lock
允许我们灵活地控制互斥量的锁定时机,从而在需要时选择性地锁定互斥量,而不是在构造锁定对象时立即锁定。这对于需要根据特定条件进行互斥操作的情况非常有用。
3.3 std::try_to_lock
- std::try_to_lock参数作用:锁管理器在构造的时候尝试lock;如果lock上,锁管理器就拥有可锁对象(持有锁),析构的时候自动执行解锁;否则就不持可锁对象,析构的时候也就不会解锁;它的好处是当某个线程尝试获取该该锁,但是该锁已经已被其他线程持有,那么不会出现线程被阻塞挂起。
- std::try_to_lock 是 std::unique_lock 用于指定在构造锁定对象时尝试非阻塞地获取互斥量的所有权,使用
std::try_to_lock
参数时,构造函数会尝试立即获取互斥量的所有权,但如果互斥量已经被其他线程锁定,则不会阻塞当前线程,而是立即返回
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
if (lock.owns_lock()) {
// 成功获取互斥量的所有权
// 在此作用域内对互斥量进行操作
// lock 在作用域结束时会自动解锁互斥量
} else {
// 未能获取互斥量的所有权
// 可以在此处理无法获取锁的情况
}
-
在上面的示例中,我们使用
std::try_to_lock
参数来构造std::unique_lock
对象lock
,并将其作为第二个参数传递给构造函数。构造函数会立即尝试获取互斥量的所有权,但如果互斥量已经被其他线程锁定,则不会阻塞当前线程,而是立即返回。 -
通过调用
lock.owns_lock()
成员函数,我们可以检查是否成功获取互斥量的所有权。如果owns_lock()
返回true
,表示成功获取了互斥量的所有权,可以在作用域内对互斥量进行操作。如果owns_lock()
返回false
,表示无法获取互斥量的所有权,可以在此处理无法获取锁的情况。使用std::try_to_lock
可以实现非阻塞地尝试获取互斥量的所有权,适用于需要在无法立即获取锁时执行备选操作的情况。
4、std::unique_lock 和 std::lock_guard 的区别
std::unique_lock 和 std::lock_guard 都是 C++ 中用于管理互斥量的 RAII(Resource Acquisition Is Initialization)类。它们的目的是确保在作用域结束时自动释放互斥量,以避免忘记手动解锁。
区别:
-
灵活性:
std::unique_lock
提供了更大的灵活性。你可以在任何时候手动锁定或解锁互斥量,也可以选择延迟锁定或尝试锁定。而std::lock_guard
则是在构造时锁定互斥量,在析构时解锁,没有手动解锁的选项。 -
所有权传递:
std::unique_lock
是可移动的,可以通过移动构造函数传递所有权,从而可以将std::unique_lock
对象传递给其他函数或线程。而std::lock_guard
没有移动构造函数,只能在其作用域内使用。 -
条件变量支持:
std::unique_lock
可以与条件变量(std::condition_variable
)一起使用,它提供了更灵活的等待和通知机制。而std::lock_guard
不能直接与条件变量一起使用,因为它没有提供相应的成员函数。 -
多个互斥量:
std::unique_lock
可以同时管理多个互斥量,通过构造函数和成员函数lock()
、try_lock()
可以实现对多个互斥量的加锁和解锁操作。而std::lock_guard
只能管理单个互斥量。 -
构造
std::unique_lock
时有3个参数(std::adopt_lock
,std::defer_lock
,std::try_to_lock
)可以选择,std::lock_guard
只有一个参数(std::adopt_lock
可以选择.
一般来说,如果你只需要简单地对单个互斥量进行加锁和解锁,并且不需要手动解锁、条件变量支持或传递所有权,那么 std::lock_guard
是更简单和推荐的选择。它更加轻量且易于使用。如果你需要更多的灵活性,例如手动解锁、延迟锁定、尝试锁定、条件变量支持或传递所有权等,那么 std::unique_lock
是更适合的选择。它提供了更多的功能和控制,但相应地会增加一些开销。
Time:2023.5.21 (周日)
如果上面代码对您有帮助,欢迎点个赞!!!