C++锁的使用

文章介绍了C++并发编程中使用互斥锁、读写锁、递归锁和自旋锁来保护共享资源,以及std::lock_guard、std::unique_lock和std::shared_lock等类的用法,强调了如何通过这些工具管理和避免死锁问题。
摘要由CSDN通过智能技术生成

在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;
}

  • 24
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值