C++11 多线程 —— 在初始化期间保护共享数据

情景假设:Suppose you have a shared resource that’s so expensive to construct that you want to do so only if it’s actually required; maybe it opens a database connection or allocates a lot of memory.

  • 解决方法1:懒汉式初始化

    std::shared_ptr<some_resource> resource_ptr;
    std::mutex resource_mutex;
    
    void foo() {
    	std::unique_lock<std::mutex> lk(resource_mutex);	// All threads are serialized(串行化) here
    	if(!resource_ptr) {
    		resource_ptr.reset(new some_resource);		// Only the initialization needs protection
    	}
    	lk.unlock();
    	resource_ptr->do_something();
    }
    

    上述代码中,只有初始化时才需要保护,而现在的问题是:每次调用 foo() 函数时,都必须先加锁(奢侈的耗时操作),即使已经初始化了也是如此。

    前人们精益求精(这是好事),为了解决这个问题,于是臭名昭著的双重检锁就诞生了。。。

  • 失败的方法:双重检锁

    void undefined_behaviour_with_double_checked_locking() {
    	if(!resource_ptr) {									// (1)
    		std::lock_guard<std::mutex> lk(resource_mutex);
    		if(!resource_ptr) {								// (2)
    			resource_ptr.reset(new some_resource);		// (3)
    		}
    	}
    	resource_ptr->do_something();						// (4)
    }
    

    (直观的表象)The pointer is first read without acquiring the lock (1), and the lock is acquired only if the pointer is NULL. The pointer is then checked again once the lock has been acquired (2) in case another thread has done the initialization between the first check and this thread acquiring the lock.

    原因)It has the potential for nasty race conditions, because the read outside the lock (1) isn’t synchronized with the write done by another thread inside the lock (3). This therefore creates a race condition that covers not just the pointer itself but also the object pointed to; even if a thread sees the pointer written by another thread, it might not see the newly created instance of some_resource, resulting in the call to do_something() operating on incorrect values.
    如,线程A在执行(1)时,线程B在执行(3),此时线程A可能读取到的数据是线程B开始写入且未写入完全的数据,之后由于线程A判断 resource_ptr 非空,所以接着执行(4),进而导致未定义行为。
    这告诉我们:不要以为只有修改数据时才需要同步、保护,读取数据时同样需要保护。因为有可能当你在读取数据时,另一个线程正在修改该数据!

  • C++ 支持
    The C++ Standards Committee also saw that this was an important scenario, and so the C++ Standard Library provides std::once_flag and std::call_once to handle this situation.

    std::call_once works with any function or callable object.

    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);	// Initialization is called exactly once
    	resource_ptr->do_something();
    }
    

    用于类成员std::call_once() can just as easily be used for lazy initialization of class members.

    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);	// 1
    		connection.send_data(data);
    	}
    	
    	data_packet receive_data() {
    		std::call_once(connection_init_flag, &X::open_connection, this);	// 2
    		return connection.receive_data();
    	}
    };
    

    In that example, the initialization is done either by the first call to send_data() or by the first call to receive_data() . The use of the member function open_connection() to initialize the data also requires that the this pointer be passed in.
    Just as for other functions in the Standard Library that accept callable objects, such as the constructor for std::thread and std::bind(), this is done by passing an additional argument to std::call_once() .


  • 静态的局部变量
    第一次遇到时执行初始化)The initialization of a static local variable is defined to occur the first time control passes through its declaration; for multiple threads calling the function, this means there’s the potential for a race condition to define first.

    每个线程要么认为自己是第一个,要么认为该变量可以直接使用了)On many pre-C++11 compilers this race condition is problematic in practice, because multiple threads may believe they’re first and try to initialize the variable, or threads may try to use it after initialization has started on another thread but before it’s finished.

    C++11解决了这个问题)In C++11 this problem is solved: the initialization is defined to happen on exactly one thread, and no other threads will proceed until that initialization is complete, so the race condition is just over which thread gets to do the initialization rather than anything more problematic.

    std::call_once的备胎)This can be used as an alternative to std::call_once for those cases where a single global instance is required:

    class my_class;
    
    my_class& get_my_class_instance() {
    	static my_class instance;		// Initialization guaranteed to be thread-safe
    	return instance;
    }
    

    Multiple threads can then call get_my_class_instance() safely, without having to worry about race conditions on the initialization.

猜你喜欢

转载自blog.csdn.net/fcku_88/article/details/88360191
今日推荐