在C++并发编程中,我们经常通过锁来保护共享资源(临界资源),防止多线程访问的时候造成数据不一致的问题。而锁的种类也有很多种,互斥锁,读写锁,递归锁,自旋锁等等。
std::mutex
多线程中最常见的,用的最多的就是互斥锁了,通过互斥锁来确保每次只有一个线程可以只有锁。
下面的示例可以验证两个线程对于共享数据的访问是独立的
std::mutex mtx; //定义互斥锁
int shared_data = 10; //共享数据
void thread_test01()
{
while (true)
{
mtx.lock();//上锁
shared_data++;
std::cout << "current_id:" << std::this_thread::get_id()<<" shared_data:"
<<shared_data << std::endl;
mtx.unlock();//解锁
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
void thread_test02()
{
while (true)
{
mtx.lock();//上锁
shared_data--;
std::cout << "current_id:" << std::this_thread::get_id()<<" shared_data:"
<<shared_data << std::endl;
mtx.unlock();//解锁
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
int main()
{
std::thread t1(thread_test01);
std::thread t2(thread_test02);
t1.join();
t2.join();
return 0;
}
std::lock_guard
std::lock_guard是一种RAII机制,即资源获取即初始化,在构造函数中会自动加锁,当离开作用域的时候,会自动调用析构函数去释放锁,这样可以避免std::mutex在调用的时候忘记了解锁导致死锁。同时,一些异常原因或者其他操作导致提前退出函数也会去释放锁。
std::lock_guard的定义方式如下:
explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
_MyMutex.lock();
}
lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock
~lock_guard() noexcept {
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
可以看到,std::lock_guard在构造函数中,自动进行了加锁操作,在析构函数中进行了解锁操作
因此可以将上述std::mutex的代码简化成下面的形式
std::mutex mtx; //定义互斥锁
int shared_data = 10; //共享数据
void thread_test01()
{
while (true)
{
std::lock_guard<std::mutex> lk(mtx);
shared_data++;
std::cout << "current_id:" << std::this_thread::get_id() << " shared_data:" << shared_data << std::endl;
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
void thread_test02()
{
while (true)
{
std::lock_guard<std::mutex> lk(mtx);
shared_data--;
std::cout << "current_id:" << std::this_thread::get_id()<<" shared_data:"<<shared_data << std::endl;
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
int main()
{
std::thread t1(thread_test01);
std::thread t2(thread_test02);
t1.join();
t2.join();
return 0;
}
std::unique_lock
std::unique_lock基础操作
std::unique_lock和std::lock_guard的使用方法基本是相同的,都是RAII机制,在构造函数中进行加锁,在析构函数中进行解锁操作,但是std::unique_lock有一点的区别在于可以手动解锁,方便控制锁的粒度(加锁的范围大小),同时也能支持跟条件变量使用
std::unique_lock的定义方式如下:
_NODISCARD_CTOR explicit unique_lock(_Mutex& _Mtx)
: _Pmtx(_STD addressof(_Mtx)), _Owns(false) { // construct and lock
_Pmtx->lock();
_Owns = true;
}
~unique_lock() noexcept {
if (_Owns) {
_Pmtx->unlock();
}
}
void lock() { // lock the mutex
_Validate();
_Pmtx->lock();
_Owns = true;
}
void unlock() {
if (!_Pmtx || !_Owns) {
_Throw_system_error(errc::operation_not_permitted);
}
_Pmtx->unlock();
_Owns = false;
}
_NODISCARD bool owns_lock() const noexcept {
return _Owns;
}
在std::unique_lock的构造函数中,会进行加锁操作,并且通过变量_Owns来判断是否持有锁,在析构函数中会判断是否持有锁,如果持有锁即_Owns为true的情况下才会去进行解锁,否则说明未持有锁则不需要进行释放。可以通过函数owns_lock来判断是否持有锁。
将上述std::lock_guard的示例改成下面的形式,只需要对共享数据shared_data进行加锁,然后可以手动解锁
std::mutex mtx; //定义互斥锁
int shared_data = 10; //共享数据
void thread_test01()
{
while (true)
{
std::unique_lock<std::mutex> lk(mtx);
shared_data++;
std::cout << "current_id:" << std::this_thread::get_id() << " shared_data:" << shared_data << std::endl;
lk.unlock();//手动解锁
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
void thread_test02()
{
while (true)
{
std::unique_lock<std::mutex> lk(mtx);
shared_data--;
std::cout << "current_id:" << std::this_thread::get_id()<<" shared_data:"<<shared_data << std::endl;
lk.unlock();//手动解锁
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
std::defer_lock延迟加锁
使用std::unique_lock可以进行延迟加锁,std::unique_lock的构造函数提供了这样的一个示例:
unique_lock(_Mutex& _Mtx, defer_lock_t) noexcept
: _Pmtx(_STD addressof(_Mtx)), _Owns(false) {} // construct but don't lock
从构造函数中可以看出,如果给了第二个参数defer_lock_t则不会默认上锁,需要手动去上锁
示例:
std::mutex mtx; //定义互斥锁
int shared_data = 10;
void thread_test()
{
std::unique_lock<std::mutex> lk(mtx,std::defer_lock);//定义独占锁,还未上锁
std::cout << lk.owns_lock() << std::endl;//获取是否持有锁,输出为0表示未持有锁
std::this_thread::sleep_for(std::chrono::seconds(1));
lk.lock();//加锁,只对共享变量shared_data进行加锁,其他操作不需要加锁,可以不用一开始就加锁,减少锁的粒度
shared_data++;
std::cout << lk.owns_lock() << std::endl;//输出为1,持有锁
//lk.unlock();//可以手动解锁,也可以不解锁,出了作用域会自动解锁
}
只在对共享数据shared_data的操作的时候进行加锁,对其他操作不需要锁,可以减少锁的粒度
std::adopt_lock领养锁
使用std::unique_lock可以领养锁,std::unique_lock的构造函数提供了这样的一个示例:
_NODISCARD_CTOR unique_lock(_Mutex& _Mtx, adopt_lock_t)
: _Pmtx(_STD addressof(_Mtx)), _Owns(true) {} // construct and assume already locked
从这个构造函数中可以看到,这个构造函数中并没有进行上锁操作,但是_Owns的状态确是已经持有锁,因此调用这个构造函数的时候,先对互斥量进行上锁,然后去收养这个锁,然后可以将锁的释放交给std::unique_lock
std::mutex mtx; //定义互斥锁
int shared_data = 10;
void thread_test1()
{
while (1)
{
mtx.lock();//上锁
std::unique_lock<std::mutex> lk(mtx, std::adopt_lock);//领养mtx
shared_data++;
std::cout << "current_id:" << std::this_thread::get_id() << " shared_data:" << shared_data << std::endl;
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
void thread_test2()
{
while (1)
{
mtx.lock();//上锁
std::unique_lock<std::mutex> lk(mtx, std::adopt_lock);//领养mtx
shared_data--;
std::cout << "current_id:" << std::this_thread::get_id() << " shared_data:" << shared_data << std::endl;
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
int main()
{
std::thread t1(thread_test1);
std::thread t2(thread_test2);
while (1);
return 0;
}
std::shared_lock
经常有这样的情景,有多个线程可以同时去进行数据的读取操作,也可以进行数据的写入操作,如果用互斥锁的话,比如去读取数据,那么一次就只能一个线程去读取,不能同时多个线程一起读取,在C语言中可以使用读写锁来进行操作,读写锁允许多个线程同时进行读操作,但是同一时间只允许一个线程进行写操作,也就是写操作的时候需要独占锁,读操作的时候可以共享锁。C++中并没有直接提供读写锁,但是可以使用std::shared_lock共享锁来实现读写锁的效果
std::shared_mutex mtx;
int shared_data = 10;
void read()
{
while (1)
{
//读操作,用共享锁,同一时间允许多个线程同时进行读操作
std::shared_lock<std::shared_mutex> lk(mtx);
std::cout << "current_id:" << std::this_thread::get_id() << " shared_data:" << shared_data << std::endl;
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
void write1()
{
while (1)
{
//写操作,独占锁,同一时间只允许一个线程进行写操作
std::unique_lock<std::shared_mutex> lk(mtx);
shared_data++;
std::cout << "current_id:" << std::this_thread::get_id() << " shared_data:" << shared_data << std::endl;
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
void write2()
{
while (1)
{
std::unique_lock<std::shared_mutex> lk(mtx);
shared_data--;
std::cout << "current_id:" << std::this_thread::get_id() << " shared_data:" << shared_data << std::endl;
std::this_thread::sleep_for(std::chrono::microseconds(10));
}
}
int main()
{
std::thread t1(read);
std::thread t2(read);
std::thread t3(write1);
std::thread t4(write2);
while (1);
return 0;
}
std::recursive_mutex
有的时候在实现函数接口的时候,在函数内部进行了加锁,内部调用完成之后在自动解锁。在调用函数接口之前可能需要加锁进行操作(比如互斥锁std::mutex),然后在调用函数接口,此时就会出现嵌套锁的情况,导致死锁,比如下面的示例
td::mutex mtx;
void mutex_test()
{
std::lock_guard<std::mutex> lk(mtx);
std::this_thread::sleep_for(std::chrono::microseconds(100));
std::cout << __FUNCTION__ << std::endl;
}
int main()
{
//加锁
std::lock_guard<std::mutex> lk(mtx);
//调用函数,函数内部也对mtx进行了加锁,此时外部的锁还未释放,导致加锁失败
mutex_test();
return 0;
}
这个时候可以使用递归锁std::recursive_mutex,递归锁源码如下所示,源码中并没有提供过多的操作函数
class recursive_mutex : public _Mutex_base { // class for recursive mutual exclusion
public:
recursive_mutex() : _Mutex_base(_Mtx_recursive) {}
_NODISCARD bool try_lock() noexcept {
return _Mutex_base::try_lock();
}
recursive_mutex(const recursive_mutex&) = delete;
recursive_mutex& operator=(const recursive_mutex&) = delete;
};
#endif // _M_CEE
将前面的示例改成递归锁的示例,这样可以正常输出内容
std::recursive_mutex mtx;
void recursive_test()
{
std::lock_guard<std::recursive_mutex> lk(mtx);
std::this_thread::sleep_for(std::chrono::microseconds(100));
std::cout << __FUNCTION__ << std::endl;
}
int main()
{
std::lock_guard<std::recursive_mutex> lk(mtx);
recursive_test();
return 0;
}
死锁
造成死锁的原因可能有一下的原因
1.加锁的顺序不一致,比如两个线程循环调用,线程1先加锁A,然后在加锁B,线程2先加锁B,然后在加锁A,那么某一时刻就有可能造成死锁,比如线程1已经完成对A的加锁,线程2已经完成对B的加锁,这个时候两个线程都希望拿到对方的锁,但是又没有释放自己的锁,导致死锁
示例
std::mutex mtx1;
std::mutex mtx2;
int data1 = 1;
int data2 = 2;
void mtx_lock1()
{
while (true)
{
std::cout << "lock1 begin" << std::endl;
mtx1.lock();
data1++;
mtx2.lock();
data2--;
mtx2.unlock();
mtx1.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "lock1 end" << std::endl;
}
}
void mtx_lock2()
{
while (true)
{
std::cout << "lock2 begin" << std::endl;
mtx2.lock();
data2--;
mtx1.lock();
data1++;
mtx1.unlock();
mtx2.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "lock2 end" << std::endl;
}
}
int main()
{
std::thread t1(mtx_lock1);
std::thread t2(mtx_lock2);
t1.join();
t2.join();
return 0;
}
这段程序在运行一段时间后一定会出现死锁的情况。
2.当嵌套加锁的时候也会出现死锁的情况, 比如前面递归锁std::recursive_mutex的示例中,当在一个函数内部加锁之后,在外部调用的时候又加锁了,这个时候也会造成死锁。
std::lock
当出现两个以上的锁的时候,使用不当的时候很容易造成死锁。c++11中提供了std::lock可以一次性锁住多个(两个以上)的互斥量,并且没有死锁风险
将上述有死锁风险的代码改成以std::lock的形式来避免死锁
std::mutex mtx1;
std::mutex mtx2;
int data1 = 1;
int data2 = 2;
void mtx_lock1()
{
while (true)
{
std::cout << "lock1 begin" << std::endl;
std::lock(mtx1,mtx2);//调用std::lock来锁住两个变量
std::unique_lock<std::mutex> lk1(mtx1,std::adopt_lock);
std::unique_lock<std::mutex> lk2(mtx2,std::adopt_lock);//将锁交给std::unique_lock管理
data1++;
data2--;
//mtx1.unlock();
//mtx2.unlock();不能调用解锁,会异常,mtx1和mtx2已经交给了std::unique_lock进行管理
//加锁和解锁的操作归std::unique_lock管理
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "lock1 end" << std::endl;
}
}
void mtx_lock2()
{
while (true)
{
std::cout << "lock2 begin" << std::endl;
std::lock(mtx1, mtx2);
std::unique_lock<std::mutex> lk1(mtx1, std::adopt_lock);
std::unique_lock<std::mutex> lk2(mtx2, std::adopt_lock);
data2--;
data1++;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "lock2 end" << std::endl;
}
}
int main()
{
std::thread t1(mtx_lock1);
std::thread t2(mtx_lock2);
t1.join();
t2.join();
return 0;
}
自旋锁
自旋锁就是不停的自我旋转,所谓的自我旋转实际上就是指while循环或者for循环不停的旋转,直到完成任务,它跟互斥锁有所区别,如果自旋锁获取不到锁就会阻塞,不停的循环尝试获取锁,直到获取锁为止
1.当一个线程尝试去获取锁的时候,如果此时锁已经被其他线程获取了,那么该线程会循环等待,不停的去尝试获取,只有成功获取锁之后,才会退出循环
2.自旋锁不会放弃CPU的时间片,线程会一直处于用户态,不停的去尝试获取锁,直到成功
3.像互斥锁这种,如果没有获取到锁,会切换线程状态,让线程休眠,使得CPU可以在这段时间去做其他事情
4.自旋锁适用于短期锁定的情况,它避免了线程挂起和上下文切换的开销,但是如果等待时间过长,自旋会浪费CPU资源
c++11中并没有直接提供自旋锁,可以借助原子操作实现一个自旋锁,这里原子操作不做过多说明,下面给出自旋锁的具体实现
//自旋锁
class spin_mutex
{
public:
spin_mutex() = default;
spin_mutex(const spin_mutex&) = delete;
spin_mutex& operator = (const spin_mutex&) = delete;
void lock()
{
while (m_flag.test_and_set(memory_order_acquire));
}
void unlock()
{
m_flag.clear(memory_order_release);
}
private:
//原子类型,表示一个布尔变量,这个类型的对象可以在两种状态之间进行切换:设置和清除
//std::atomic_flag必须使用ATOMIC_FLAG_INIT初始化,状态为清除
std::atomic_flag m_flag = ATOMIC_FLAG_INIT;
};
使用示例:
int value = 1;
spin_mutex spin_mtx;
void spin_test()
{
for (int i = 0; i < 10; i++)
{
std::lock_guard<spin_mutex> lk(spin_mtx);
value++;
std::cout << "thread_id:" << std::this_thread::get_id() << " value:" << value << std::endl;
}
}
int main()
{
std::thread t1(spin_test);
std::thread t2(spin_test);
t1.join();
t2.join();
return 0;
}