来源:多线程学习
互斥量死锁
假设有两个线程 T1 和 T2,它们需要对两个互斥量 mtx1 和 mtx2 进行访问,而且需要按照以下顺序获取互斥量的所有权:
- T1 先获取 mtx1 的所有权,再获取 mtx2 的所有权。
- T2 先获取 mtx2 的所有权,再获取 mtx1 的所有权。
如果两个线程同时执行,就会出现死锁问题。因为 T1 获取了 mtx1 的所有权,但是无法获取 mtx2 的所有权,而 T2 获取了 mtx2 的所有权,但是无法获取 mtx1 的所有权,两个线程互相等待对方释放互斥量,导致死锁。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int share_data = 0;
mutex mtx1;
mutex mtx2;
void fun1() {
for (int i = 0; i < 100000; ++i) {
mtx1.lock();
mtx2.lock();
mtx1.unlock();
mtx2.unlock();
}
}
void fun2() {
for (int i = 0; i < 100000; ++i) {
mtx2.lock();
mtx1.lock();
mtx2.unlock();
mtx1.unlock();
}
}
int main() {
thread t1(fun1);
thread t2(fun2);
t1.join();
t2.join();
cout << share_data << endl;
return 0;
}
解决该问题的方法,可以让两个线程按照相同的顺序获取互斥量的所有权。例如,都先获取 mtx1 的所有权,再获取 mtx2 的所有权,或者都先获取 mtx2 的所有权,再获取 mtx1 的所有权。这样就可以避免死锁问题。
原因在于每个程序在运行的时候,都要先判断mtx1是否被占用,如果被占用就不能继续往下了,也就取不到mtx2了,而如果没有被占用则就可以继续往下。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int share_data = 0;
mutex mtx1;
mutex mtx2;
void fun1() {
for (int i = 0; i < 100000; ++i) {
mtx1.lock();
mtx2.lock();
mtx1.unlock();
mtx2.unlock();
}
}
void fun2() {
for (int i = 0; i < 100000; ++i) {
mtx1.lock();
mtx2.lock();
mtx2.unlock();
mtx1.unlock();
}
}
int main() {
thread t1(fun1);
thread t2(fun2);
t1.join();
t2.join();
cout << share_data << endl;
return 0;
}
lock_guard 与 unique_lock
lock_guard
lock_guard是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题。
lock_guard的特点是:
-
当构造函数被调用时,该互斥量会被自动锁定。
-
当析构函数被调用时,该互斥量会被自动解锁。
-
lock_guard对象不能复制或移动,因此它只能在局部作用域中使用。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int share_data = 0;
mutex mtx;
void fun() {
for (int i = 0; i < 1000000; ++i) {
//创建即加锁
lock_guard<mutex> lg(mtx);
share_data += 1;
}
}
int main() {
thread t1(fun);
thread t2(fun);
t1.join();
t2.join();
cout << share_data << endl;
return 0;
}
lock_guard还有第二种构造函数,除了传递mtx以外,还可以传递一个参数adopt_lock,表示之前以及上过锁了。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int share_data = 0;
mutex mtx;
void fun() {
for (int i = 0; i < 1000000; ++i) {
//创建即加锁
mtx.lock();
lock_guard<mutex> lg(mtx, adopt_lock);
share_data += 1;
}
}
int main() {
thread t1(fun);
thread t2(fun);
t1.join();
t2.join();
cout << share_data << endl;
return 0;
}
unique_lock
unique_lock 是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。
unique_lock提供了以下几个成员函数:
- lock():尝试对互斥量进行加锁操作(这是个手动加锁的操作),如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。
- try_lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回false,否则返回true,和lock()不同的是,使用try_lock()不会发生堵塞。
- try_lock_for(const chrono::duration(Rep, Period>& rel_time) :与上一个函数相比,增加了一个时间参数,也是尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被另一个线程解锁,当前线程成功加锁,或者超过了给定的等待时间。
- try_lock_until(const chrono::time_point<Clock, Duration>& abs_time):和上一个函数效果类似,不同的在于,增加的参数是一个时间点,效果是对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。
- unlock():对互斥量进行解锁操作(手动解锁)
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int share_data = 0;
//由于后续要使用到时间操作,因此这里我们需要使用到时间锁
timed_mutex mtx;
void fun() {
for (int i = 0; i < 10; ++i) {
//增加defer_lock参数,表示创建锁,但是不进行枷锁操作
unique_lock<timed_mutex> lg(mtx, defer_lock);
//try_lock_for返回的是一个bool值,成功加锁返回true,否则返回false
if (lg.try_lock_for(chrono::seconds(1))) {
share_data += 1;
//表示当前线程休眠2s
this_thread::sleep_for(chrono::seconds(2));
}
}
}
int main() {
thread t1(fun);
thread t2(fun);
t1.join();
t2.join();
cout << share_data << endl;
return 0;
}
unique_lock提供了以下几个构造函数:
- unique_lock() noexcept = default:默认构造函数,创建一个未关联任何互斥量的unique_lock对象。
- explicit unique_lock(mutex_type& m):构造函数,使用给定的互斥量m 进行初始化,并默认对该互斥量进行枷锁操作。(只支持显示构造)
- unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量m 进行初始化,但是不对该互斥量进行加锁操作。
- unique_lock(mutex_type& m, try_lock_t) noexcept:构造函数,使用给定的互斥量m 进行初始化,并尝试对该互斥量进行加锁,如果加锁失败,则创建的unqiue_lock 对象不与任何互斥量关联。
- unique_lock(mutex_type& m, adopt_lock_t) noexcept:构造函数,使用给定的互斥量m 进行初始化,并假设互斥量已经被当前线程成功加锁。
总结:相比于lock_guard,unqiue_lock使用上非常灵活方便。
call_once与其使用场景
该使用场景主要就是单例设计模式。单例设计模式是一种常见的设计模式,用于确保某个类只能创建一个实例。由于单例实例是全局唯一的,因此在多线程环境中使用单例模式时,需要考虑线程安全的问题。
代码实现:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
class Log {
public:
Log() {};
//单例模式,类只能有一个对象,因此我们需要禁用其拷贝构造和等于号
Log(const Log& log) = delete;
Log& operator=(const Log& log) = delete;
//全局只需要一个,我们可以采用静态构造的方法
//static Log& GetInstance() {
// static Log log; //饿汉单例模式
// return log;
//}
static Log& GetInstance() {
static Log* log = nullptr; //懒汉模式
if (!log) log = new Log;
return *log;
}
void PrintLog(string msg) {
cout << __TIME__ << " " << msg << endl;
}
};
int main() {
Log::GetInstance().PrintLog("这里有问题");
return 0;
}
上述中,我们创建了一个日志类,并给了懒汉模式和饿汉模式两种静态构造方法。 由于静态局部变量只会被初始化一次,因此该实现可以确保单例实例只会被创建一次。
但是,该实现并不是线程安全的。如果多个线程同时调用 getInstance()
函数,可能会导致多个对象被创建,从而违反了单例模式的要求。
为例解决该问题,我们可以使用call_once来实现一次性初始化,从而确保单例实例只会被创建一次。
call_once函数的原型为:void call_once(std::once_flag& flag, Callable&& func, Args&&... args);
其中`flag` 是一个 `std::once_flag` 类型的对象,用于标记函数是否已经被调用;`func` 是需要被调用的函数或可调用对象;`args` 是函数或可调用对象的参数。
`std::call_once` 的作用是,确保在多个线程中同时调用 `call_once` 时,只有一个线程能够成功执行 `func` 函数,而其他线程则会等待该函数执行完成。
class Singleton {
public:
static Singleton& getInstance() {
std::call_once(m_onceFlag, &Singleton::init);
return *m_instance;
}
void setData(int data) {
m_data = data;
}
int getData() const {
return m_data;
}
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static void init() {
m_instance.reset(new Singleton);
}
static std::unique_ptr<Singleton> m_instance;
static std::once_flag m_onceFlag;
int m_data = 0;
};
std::unique_ptr<Singleton> Singleton::m_instance;
std::once_flag Singleton::m_onceFlag;
在这个实现中,我们使用了一个静态成员变量 m_instance
来存储单例实例,使用了一个静态成员变量 m_onceFlag
来标记初始化是否已经完成。在 getInstance()
函数中,我们使用 std::call_once
来调用 init()
函数,确保单例实例只会被创建一次。在 init()
函数中,我们使用了 std::unique_ptr
来创建单例实例。
使用 std::call_once
可以确保单例实例只会被创建一次,从而避免了多个对象被创建的问题。此外,使用 std::unique_ptr
可以确保单例实例被正确地释放,避免了内存泄漏的问题。