《C++ Concurrency in Action》笔记11 同步初始化

有时,不太好确定锁定的粒度究竟应该多大,因为并不是所有对共享数据结构的请求都在一个层级上。此时,另一种机制也许可以代替普通的mutex。

一种极端的情况是,共享数据仅仅在初始化时需要同步,初始化之后就再也不需要做同步操作了。这也许是因为:要么这是一种只读数据;要么它的所有操作都隐含着相应的保护机制。再者,纯粹为了保护数据初始化而使用mutex是没必要的,也是有损性能的。为此,C++标准提供了一种机制,它纯粹就是为了初始化。

想象一下这种情况,你有一种共享数据资源,它的构造很耗时,以至于你只想在真的需要它时才去构造,例如:数据库连接,或者申请大块内存。惰性初始化(Lazy initialization)经常用于单线程开发中:每个请求资源的操作首先检查它是否已经初始化,如果没有就去初始化它,然后再使用:

std::shared_ptr<some_resource> resource_ptr;
void foo()
{
    if (!resource_ptr)
    {
        resource_ptr.reset(new some_resource);
    }
    resource_ptr->do_something();
}
如果想将上面的过程改为线程安全的,那么唯一需要修改的就是初始化那句。但是一种天真的做法可能会导致意想不到的问题:

std::shared_ptr<some_resource> resource_ptr;
std::mutex resource_mutex;
void foo()
{
	std::unique_lock<std::mutex> lk(resource_mutex);
	if (!resource_ptr)
	{
		resource_ptr.reset(new some_resource);
	}
	lk.unlock();
	resource_ptr->do_something();
}
这样改会导致多线程访问这个函数时产生不必要的序列化。每个线程都需要等待锁定mutex,以检查资源是否已经初始化。

很多人尝试解决这个问题,包括声名狼藉的双重检查锁(Double-Checked Locking)模式:

void undefined_behaviour_with_double_checked_locking()
{
	if (!resource_ptr)
	{
		std::lock_guard<std::mutex> lk(resource_mutex);
		if (!resource_ptr)
		{
			resource_ptr.reset(new some_resource);
		}
	}
	resource_ptr->do_something();
}
原因在于,第一次检查resource_ptr的时候以及给resource_ptr初始化时没有进行同步。看下面的分析:

obj *p = new obj()  
为了执行这句代码,机器需要做三样事儿:1.为obj对象分配空间。2.在分配的空间中构造对象。3.使p指向分配的空间。遗憾的是编译器并不是严格按照上面的顺序来执行的。可以交换2和3。

如果编译器将执行顺序更改了,可想而知,上面的程序有可能导致指针已经被赋值,但是对象还未构造,却被另一个线程认为可用并且直接使用了。

C++标准委员会注意到了这种情况,因此提供了std::once_flag和std::call_once来解决这类问题。call_once()函数接受一个once_flag对象参数,接受一个谓词参数或函数,以及不定个数的参数。它保证指定的谓词或函数只执行一次,而且其效率要远远高于直接使用mutex。看下面的代码:

std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;
void init_resource()
{
	resource_ptr.reset(new some_resource);
}
void foo()
{
	std::call_once(resource_flag, init_resource);
	resource_ptr->do_something();
}

它保证init_resource()只被执行了一次,不管多少线程同时调用。而且代码很简单。call_once()可以很容易的包装到类中,看下面的代码:

class X
{
private:
	connection_info connection_details;
	connection_handle connection;
	std::once_flag connection_init_flag;
	void open_connection()
	{
		connection = connection_manager.open(connection_details);
	}
public:
	X(connection_info const& connection_details_) :
		connection_details(connection_details_)
	{}
	void send_data(data_packet const& data)
	{
		std::call_once(connection_init_flag, &X::open_connection, this);
		connection.send_data(data);
	}
	data_packet receive_data()
	{
		std::call_once(connection_init_flag, &X::open_connection, this);
		return connection.receive_data();
	}
};
std::mutex和std::once_flag都是不可以copy,也不可以move的,因此当将它们作为成员变量时,如果需要copy或者move操作,则需要手动定义对应的成员函数。

还有一种情况可以导致数据竞争问题,就是在局部static变量。在比C++11早些的编译器上,如果多个线程同时访问带有第定义局部static变量的代码,则它们可能都会去初始化它,或者试图访问一个变量,这个变量被另一个线程刚初始化但是还未完成。这个问题在C++11中已经被解决了:初始化过程被定义在一个确定的线程中,在初始化完成前,其他线程无法处理数据。例如下面这样:

class my_class;
my_class& get_my_class_instance()
{
	static my_class instance;
	return instance;
}
现在唯一需要担心的就是,在初始化过程中,如果有涉及到共享数据更新,则需要做同步保护。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值