《C++并发编程实战》之共享数据

一、std::lock_guard和std::mutex 的用法

功能介绍

二者均属于C++11的特性:

  • std::mutex属于C++11中对操作系统锁的最常用的一种封装,可以通过lock、unlock等接口实现对数据的锁定保护。

  • std::lock_guard是C++11提供的锁管理器,可以管理std::mutex,也可以管理其他常见类型的锁。

  • std::lock_guard的对锁的管理属于RAII风格用法(Resource Acquisition Is Initialization),在构造函数中自动绑定它的互斥体并加锁,在析构函数中解锁,大大减少了死锁的风险。

代码示例

#include <iostream>
#include <mutex>
#include <thread>

int loop_cnt = 10000000;

class Widget
{
public:
	Widget() = default;
	~Widget() = default;

	void addCnt()
	{
		std::lock_guard<std::mutex> cLockGurad(lock_); //构造时加锁,析构时解锁

		// lock_.lock(); //不使用lock_guard时的写法
		cnt++;
		// lock_.unlock();//不使用lock_guard时的写法,万一没有解锁就会死锁。
	}

	int cnt = 0;

private:
	std::mutex lock_;
};

void ThreadMain1(Widget *pw)
{
	std::cout << "thread 1 begin." << "\n";
	for (int i = 0; i < loop_cnt; i++)
		pw->addCnt();
	std::cout << "thread 1 end."  << "\n";
}

void ThreadMain2(Widget *pw)
{
	std::cout << "thread 2 begin."  << "\n";
	for (int i = 0; i < loop_cnt; i++)
		pw->addCnt();
	std::cout << "thread 2 end."  << "\n";
}

int main()
{
	Widget *pw = new Widget();

	std::thread t1(&ThreadMain1, pw);
	std::thread t2(&ThreadMain2, pw);

	t1.join();
	t2.join();

	std::cout << "cnt =" << pw->cnt << std::endl;
	
	return 0;
}

二、unique_lock

std::unique_lock比std::lock_guard更灵活,这种灵活性主要体现在以下几点:

  1. lock_guard在构造时或者构造前(std::adopt_lock)就已经获取互斥锁,并且在作用域内保持获取锁的状态,直到作用域结束;而unique_lock在构造时或者构造后(std::defer_lock)获取锁,在作用域范围内可以手动获取锁和释放锁,作用域结束时如果已经获取锁则自动释放锁。
  2. lock_guard锁的持有只能在lock_guard对象的作用域范围内,作用域范围之外锁被释放,而unique_lock对象支持移动操作,可以将unique_lock对象通过函数返回值返回,这样锁就转移到外部unique_lock对象中,延长锁的持有时间。
     

int n;
std::mutex some_mutex;

void prepare_data()
{
    cout << n++ << endl;
}

void do_something()
{
    cout << n++ << endl;
}

std::unique_lock<std::mutex> get_lock()
{
    std::unique_lock<std::mutex> lk(some_mutex);//与lock_guard相同,构造时获取锁
    cout << "owns_lock? " << lk.owns_lock() << endl;//1
    prepare_data();
    return lk;
}

int main()
{
    //unique_lock基本使用
    std::mutex mutex2;
    std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);//告诉构造函数暂不获取锁
    cout << "owns_lock? " << lock2.owns_lock() << endl;//0
    lock2.lock();//手动获取锁
    std::cout << "owns_lock? " << lock2.owns_lock() << endl;//1
    lock2.unlock();//手动解锁
    cout << "owns_lock? " << lock2.owns_lock() << endl;//0
    //锁所有权转移到函数外部
    std::unique_lock<std::mutex> lk(get_lock());//
    do_something();
}
//析构
//lock2未获取锁mutex2,因此不会调用unlock
//lk对象持有锁some_mutex,调用unlock

由于unique_lock对象需要根据当前对象是否已经持有锁还是未持有进行判断从而执行适当的操作,因此比lock_guard占用空间稍大一点,效率稍低一点,std::unique_lock.owns_lock返回当前是否持有锁。通常来说不应该在持有锁的期间执行消耗时间长的操作,此时unique_lock更加灵活,可以随时unlock,避免不相关的操作期间仍然持有锁。

std::unique_lock<std::mutex> my_lock(the_mutex);
some_class data_to_process = get_next_data_chunk();
my_lock.unlock();
result_type result = process(data_to_process);
my_lock.lock();
write_result(data_to_process, result);

总结:

  • std::unique_lock能够在代码不需要访问数据时调用unlock(),在代码又需要访问时再次调用lock()。
  • 延迟锁定和所有权转移的可实行原因均为std::unique_lock实例并没有拥有与其相关的互斥元。
  • 对于锁的所有权转移,左值(实变量或者对实变量的引用)的所有权转移必须通过调用std::move显式实现,以避免从变量中意外地转移了所有权。右值(某种临时变量)的所有权转移是自动的(编译器负责调用移动构造函数)。
  • std::unique_lock是可移动但不可复制的类型。
  • 允许实例在被销毁前撤回它们的锁。(可以调用std::lock() std::unlock()来锁定和解锁成员函数集合)

三.不同域中互斥量的传递

std::unique_lock实例没有与自身相关的互斥量,互斥量的所有权可以通过移动操作,在不同的实例中进行传递。某些情况下,这种转移是自动发生的,例如:当函数返回一个实例。另一种情况下,需要显式的调用std::move()来执行移动操作。本质上来说,需要依赖于源值是否是左值——一个实际的值或是引用——或一个右值——一个临时类型。当源值是一个右值,为了避免转移所有权过程出错,就必须显式移动成左值。std::unique_lock是可移动,但不可赋值的类型。

一种使用可能是允许函数去锁住一个互斥量,并且将所有权移到调用者上,所以调用者可以在这个锁保护的范围内执行额外的动作。

下面的程序片段展示了:函数get_lock()锁住了互斥量,然后准备数据,返回锁的调用函数。

std::unique_lock<std::mutex> get_lock()
{
extern std::mutex some_mutex;
std::unique_lock<std::mutex> lk(some_mutex);
prepare_data();
return lk;
// 1
}
void process_data()
{
std::unique_lock<std::mutex> lk(get_lock());
// 2
do_something();
}

lk在函数中被声明为自动变量,它不需要调用 std::move()process_data()函数直接转移 实例的所有权2,调用do_something()可使用的正确数据(数据std::unique_lock,可以直接返回1(编译器负责调用移动构造函数)。没有受到其他线程的修改)。

四.保护共享数据的初始化过程

std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag; // 1

void init_resource()
{
    resource_ptr.reset(new some_resource);
}

void foo()
{
    std::call_once(resource_flag,init_resource);// 可以完整的进行一次初始化
    resource_ptr->do_something();
}

 这个例子中,std::once_flag①和初始化好的数据都是命名空间区域的对象,但std::call_once()可仅作为延迟初始化的类型成员。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值