• 线程安全的对象生命周期管理


    线程安全的 class 应当满足的条件

    • 多个线程同时访问时,其表现出正确的行为
    • 无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织
    • 调用端代码无须额外的同步或其他协调动作

    对象创建的线程安全

    对象创建要做到线程安全,唯一的要求就是在构造期间不要泄露 this 指针:

    • 不要在构造函数中注册任何回调
    • 不要在构造函数中把 this 传递给跨线程的对象

    因为构造函数函数在执行期间还没有完成对象的初始化,如果 this 被泄露给其它对象(其自身创建的子对象除外),那么别的线程有可能访问这个半成品对象。

    例如下面的写法是强烈不推荐的:

    class Foo
    {
    public:
    	Foo(Observable *s)
    	{
    		s->register(this);
    	}
    };
    

    正确的写法:

    class Foo
    {
    public:
    	Foo()
    	{		
    	}
    	
    	//@ 先构造再注册
    	void observer(Observable *s)
    	{
    		s->register(this);
    	}
    };
    

    析构函数的多线程安全问题

    当一个对象被多个线程可见时,对象的销毁时机可能造成竞态条件:

    • 析构一个对象时,如何得知此刻是否有别的线程正在执行该对象的成员函数?
    • 在调用一个对象的成员函数之前,如何确保这个对象还存在,它的析构函数是否会碰巧执行了一半?
    • 如何保证一个对象的成员函数执行期间,该对象不会被其他线程析构?

    mutex 不是解决办法

    例如:

    class Test final
    {
    public:
    
    	Test()
    	{ 
    		p_ = new int(0);
    
    		std::cout << "ctor" << std::endl;
    	}
    
    	~Test() 
    	{ 
    		std::lock_guard<std::mutex> lock(m_);
    		delete p_;
    		p_ = nullptr;
    
    		std::cout << "dctor" << std::endl; 
    	}
    
    	void update(int x)
    	{
    		std::lock_guard<std::mutex> lock(m_);
    		*p_ = x;
    	}
    
    private:
    	std::mutex m_;
    	int* p_;
    };
    
    Test* g_test_p = new Test;
    
    
    void f1()
    {
    	if (g_test_p != nullptr)
    	{
    		std::cout << "update" << std::endl;
    		g_test_p->update(100);
    	}
    }
    
    void f2()
    {
    	delete g_test_p;
    	g_test_p = nullptr;
    }
    
    int main()
    {
    	std::thread t2(f2);
    	std::thread t1(f1);
    
    	t1.join();
    	t2.join();
    
        return 0;
    }
    

    作为 class 数据成员的 mutex 只能用于同步本 class 的其他数据成员的读和写,不能保证安全的析构。因为 mutex 成员的生命周期最多和对象一样长,而析构动作可以说是发生在对象的身亡之时(之后)。

    对于基类对象,调用到基类析构函数时,派生类对象已经析构了,那么基类对象的 mutex 就不能完整的保护整个析构过程。

    析构过程本质上来说,也不应该被 mutex 保护,因为只有保证别的线程访问不到这个对象时,析构才是安全的。即要想安全地销毁对象,最好在别的线程都看不到的情况下,偷偷地做。

    shared_ptr/weak_ptr 解决方案

    shared_ptr 是基于引用计数的,引用计数是自动化资源管理的常用方法,当引用计数降为 0 时,对象就被销毁。weak_ptr 也是一个计数型智能指针,但是它不增加引用计数,属于弱引用。

    • shared_ptr 控制对象的生命周期,只要有一个指向对象的 shared_ptr 存在,该对象就不会析构,当指向对象的最后一个 shared_ptr 析构或者 reset 时候,对象保证会被销毁

    • weak_ptr 不控制对象的生命周期,但是它知道对象是否还存在,如果对象存在,它可以提升为有效的 shared_ptr,如果对象不存在,则提升失败,返回一个空的 shared_ptr,提升的行为是线程安全的

    shared_ptr 本身的线程安全性

    shared_ptr 的引用计数是安全且无锁的,但是它本身不是线程安全的,要在多个线程中同时访问同一个 shared_ptr,正确的用法是加 mutex 保护。

    std::mutex g_mutex;
    std::shared_ptr<Foo> g_ptr;
    
    
    void do_it(const std::shared_ptr<Foo>& p){}
    
    //@ 读取时需要加锁
    void read()
    {
    	std::shared_ptr<Foo> local_ptr;
    	{
    		std::lock_guard<std::mutex> lock(g_mutex);
    		local_ptr = g_ptr;
    	}
    	do_it(local_ptr);
    }
    
    //@ 写入时需要加锁
    void write()
    {
    	std::shared_ptr<Foo> new_ptr(new Foo);
    	{
    		std::lock_guard<std::mutex> lock(g_mutex);
    		g_ptr = new_ptr;
    	}
    	do_it(new_ptr);
    }
    

    shared_ptr 技术与陷阱

    意外延长对象的生命周期

    只有指向对象的 shared_ptr 有一个存在,对象就不会释放,从而在一些情况下导致对象的生命周期意外延长。

    class Foo 
    {
    public:
    	Foo() { std::cout << "ctor" << std::endl; }
    	~Foo() { std::cout << "dctor" << std::endl; }
    
    	void do_it() { std::cout << "do_it" << std::endl; }
    };
    
    int main()
    {
    	std::shared_ptr<Foo> pFoo(new Foo);
    	auto func = std::bind(&Foo::do_it, pFoo);
    
    	//@ do something else
    	return 0;
    }
    

    传参

    执行 shared_ptr 的拷贝时需要修改引用计数,这个开销要比拷贝原始指针高,多数情况下可以使用 const reference 的方式传递,一个线程只需要在最外层函数有一个实体对象,之后都可以使用 const reference 的方式传递这个对象。

    void save(const std::shared_ptr<Foo>& pFoo) {}
    void validate(const std::shared_ptr<Foo>& pFoo) {}
    
    void on_message(const std::string& msg)
    {
    	std::shared_ptr<Foo> pFoo(new Foo(msg));
    	if (validate(pFoo))  //@ 没有拷贝 pFoo
    	{
    		save(pFoo); //@ 没有拷贝 pFoo
    	}
    }
    

    析构动作在创建时被捕获

    • 虚析构不再是必须的
    • shared_ptr<void> 可以持有任何对象,而且能够安全释放
    • shared_ptr 对象可以安全地跨越模块边界,比如从 dll 中返回,而不会造成模块 A 分配的内存在模块 B 中释放的情况
    • 二进制兼容性,即便 shared_ptr 指向的对象大小改变了,那么旧的客户代码仍然可以使用新的库,而无须重新编译
    • 析构动作可以定制

    虚析构不是必须的

    class Base
    {
    public:
    	~Base() { std::cout << "base dctor" << std::endl; }
    };
    
    class Derived : public Base
    {
    public:
    	~Derived() { std::cout << "derived dctor" << std::endl; }
    };
    
    int main()
    {
    	//@ 使用智能指针可以正确释放
    	{
    		std::shared_ptr<Base> p1(new Derived);
    	}
    	std::cout << "-------------------------------------" << std::endl;
    
    	//@ 普通指针,当基类的析构函数不是虚函数时,子类的析构函数不会被调用
    	Base* p2 = new Derived;
    	delete p2;
    }
    

    shared_ptr<void> 可以持有任何对象

    class Foo
    {
    public:
    	Foo()
    	{
    		std::cout << "Foo ctor" << std::endl;
    	}
    
    	~Foo()
    	{
    		std::cout << "Foo dctor" << std::endl;
    	}
    
    private:
    	int * p;
    };
    
    int main(int argc, const char** argv)
    {
    	//@ 并不会调用 Foo 的析构函数,导致资源泄露
    	{
    		void * p1 = new Foo;
    		delete p1;
    	}
    	std::cout << "---------------------------------" << std::endl;
    
    	//@ 会调用 Foo 的析构函数
    	{
    		std::shared_ptr<void> p3 = std::shared_ptr<Foo>(new Foo);
    	}
    }
    

    析构所在的线程

    对象的析构是同步的,当最后一个指向对象的 shared_ptr 离开其作用域的时候,对象就会在同一个线程析构,这个线程不一定是对象诞生的线程,如果对象的析构十分耗时,那么可能会拖慢关键线程的速度,可以使用一个单独的线程专门处理析构,通过一个 BlockingQueue<shared_ptr<void>> 把对象的析构转移到专门的线程。

    定制析构函数

    假设:

    class Stock final
    {
    public:
    	explicit Stock(const std::string& key) :key_(key) {}
    	std::string key() const {
    		return key_;
    	}
    private:
    	std::string key_;
    };
    
    class StockFactory
    {
    public:
    	std::shared_ptr<Stock> get(const std::string& key) {
    		std::lock_guard<std::mutex> lock(mtx_);
    		if (stocks_.find(key) == stocks_.end()) {
    			stocks_[key] = std::make_shared<Stock>(key);
    		}
    		return stocks_[key];
    	}
    
    private:
    	mutable std::mutex mtx_;
    	std::map<std::string, std::shared_ptr<Stock>> stocks_;
    };
    

    这种写法导致的结果就是 map 中存放的 shared_ptr 一直不会被释放,Stock 对象不会被销毁。

    修改为:

    class Stock final
    {
    public:
    	explicit Stock(const std::string& key) :key_(key) {}
    	std::string key() const {
    		return key_;
    	}
    private:
    	std::string key_;
    };
    
    
    class StockFactory
    {
    public:
    	std::shared_ptr<Stock> get(const std::string& key) {
    		std::shared_ptr<Stock> pStock;
    		std::lock_guard<std::mutex> lock(mtx_);
    		auto& wkStock = stocks_[key]; //@ 引用
    		pStock = wkStock.lock(); //@ 提升
    		if (!pStock) {
    			pStock.reset(new Stock(key));
    			wkStock = pStock; //@ 更新 map
    		}
    		return pStock;
    	}
    
    private:
    	mutable std::mutex mtx_;
    	std::map<std::string, std::weak_ptr<Stock>> stocks_;
    };
    

    这样 Stock 对象会被销毁,但是 map 的大小只增不减。

    终极的解决办法是定制 shared_ptr 的删除函数:

    class Stock final
    {
    public:
    	explicit Stock(const std::string& key) :key_(key) {}
    	std::string key() const {
    		return key_;
    	}
    private:
    	std::string key_;
    };
    
    class StockFactory
    {
    public:
    	std::shared_ptr<Stock> get(const std::string& key) {
    		std::shared_ptr<Stock> pStock;
    		std::lock_guard<std::mutex> lock(mtx_);
    		auto& wkStock = stocks_[key];  //@ 引用
    		pStock = wkStock.lock(); //@ 提升
    		if (!pStock) {
    			using std::placeholders::_1;
    			pStock.reset(new Stock(key),std::bind(&StockFactory::delete_stock,this,_1));
    			wkStock = pStock; //@ 更新 map
    		}	
    		return pStock;
    	}
    
    private:
    	void delete_stock(Stock* stock) {
    		if (stock)
    		{
    			std::lock_guard<std::mutex> lock(mtx_);
    			stocks_.erase(stock->key());
    		}
    
    		delete stock;
    	}
    private:
    	mutable std::mutex mtx_;
    	std::map<std::string, std::weak_ptr<Stock>> stocks_;
    };
    

    但是这里将 StockFactory 的 this 指针传递给了 bind,如果 StockFactory 先于 Stock 对象析构,那么当 Stock 析构的时候将会发生错误。

    enable_shared_from_this

    继承自 enable_shared_from_this 的类,可以将 this 指针变成 shared_ptr:

    class Stock final
    {
    public:
    	explicit Stock(const std::string& key) :key_(key) {}
    	std::string key() const {
    		return key_;
    	}
    private:
    	std::string key_;
    };
    
    class StockFactory  : public std::enable_shared_from_this<StockFactory>
    {
    public:
    	std::shared_ptr<Stock> get(const std::string& key) {
    		std::shared_ptr<Stock> pStock;
    		std::lock_guard<std::mutex> lock(mtx_);
    		auto& wkStock = stocks_[key];  //@ 引用
    		pStock = wkStock.lock(); //@ 提升
    		if (!pStock) {
    			using std::placeholders::_1;
    			pStock.reset(new Stock(key), std::bind(&StockFactory::delete_stock, shared_from_this(), _1)); //@ 更新成 shared_from_this
    			wkStock = pStock; //@ 更新 map
    		}
    		return pStock;
    	}
    
    private:
    	void delete_stock(Stock* stock) {
    		if (stock)
    		{
    			std::lock_guard<std::mutex> lock(mtx_);
    			stocks_.erase(stock->key());
    		}
    
    		delete stock;
    	}
    private:
    	mutable std::mutex mtx_;
    	std::map<std::string, std::weak_ptr<Stock>> stocks_;
    };
    

    为了使用 shared_from_this,StockFactory 不能是栈上对象,必须是堆上对象,并且由 shared_ptr 管理其生命周期:

    //@ OK
    {
        std::shared_ptr<StockFactory> sf = std::shared_ptr<StockFactory>(new StockFactory);
        sf->get("zgpn");
    }
    
    //@ Error
    {
        StockFactory* sf = new StockFactory;
        sf->get("zgpn");
    }
    
    //@ Error
    {
        StockFactory sf;
        sf.get("zgpn");
    }
    

    另外需要注意的是,shared_from_this 不能在构造函数中调用,因为在构造过程中对象还没有交给 shared_ptr 接管。

    另外,一点就是 StockFactory 的生命周期似乎被意外延长了。shared_ptr 绑定到函数对象之后,那么回调的时候 StockFactory 对象始终存在,是安全的。但同时也使得 StockFactory 的生命周期不会短于函数对象。

    弱回调

    如果希望实现对象活着就调用对象,否则忽略之,这种方式称为 "弱回调"。这个技术的关键就是使用 weak_ptr:

    class Stock final
    {
    public:
    	explicit Stock(const std::string& key) :key_(key) {}
    	std::string key() const {
    		return key_;
    	}
    private:
    	std::string key_;
    };
    
    class StockFactory  :  public std::enable_shared_from_this<StockFactory>
    {
    public:
    	std::shared_ptr<Stock> get(const std::string& key) {
    		std::shared_ptr<Stock> pStock;
    		std::lock_guard<std::mutex> lock(mtx_);
    		auto& wkStock = stocks_[key];
    		pStock = wkStock.lock(); //@ 提升
    		if (!pStock) {
    			using std::placeholders::_1;
    			pStock.reset(new Stock(key), std::bind(&StockFactory::weak_delete_callback, std::weak_ptr<StockFactory>(shared_from_this()), _1)); //@ 更新成 shared_from_this
    			wkStock = pStock; //@ 更新 map
    		}
    		return pStock;
    	}
    
    private:
    	static void weak_delete_callback(const std::weak_ptr<StockFactory>& wkFactory,Stock* stock) 
    	{
    		std::shared_ptr<StockFactory> pFactory = wkFactory.lock();
    		if (pFactory)
    			pFactory->remove_stock(stock);
    
    		delete stock;
    	}
    
    	void remove_stock(Stock* stock) 
    	{
    		if (stock)
    		{
    			std::lock_guard<std::mutex> lock(mtx_);
    			stocks_.erase(stock->key());
    		}
    	}
    private:
    	mutable std::mutex mtx_;
    	std::map<std::string, std::weak_ptr<Stock>> stocks_;
    };
    
    void test_long_life_factory()
    {
    	std::shared_ptr<StockFactory> factory(new StockFactory);
    	{
    		std::shared_ptr<Stock> stock1 = factory->get("IBM");
    		std::shared_ptr<Stock> stock2 = factory->get("IBM");
    		assert(stock1 == stock2);
    	}
    }
    
    void test_short_life_factory()
    {
    	std::shared_ptr<Stock> stock1;
    	{
    		std::shared_ptr<StockFactory> factory(new StockFactory);
    		stock1 = factory->get("IBM");
    		std::shared_ptr<Stock> stock2 = factory->get("IBM");
    		assert(stock1 == stock2);
    	}
    }
    
    
    
    int main()
    {
    	test_long_life_factory();
    
    	test_short_life_factory();
    
    	return 0;
    }
    

    由此,无论 Stock 和 StockFactory 谁先挂掉,都不会影响程序的正常运行。

  • 相关阅读:
    dotnet run urls=http://*:8080
    内敛声明变量
    C#反射赋值(更新时用)
    mysql---auto_increment=10啥意思呢?
    vscode--代码折叠与展开
    联想启天M410台式机的清除BIOS密码
    AntD是什么?
    react-router-dom 中文文档
    Recoil是什么?
    sql server查看建表的创建代码?
  • 原文地址:https://www.cnblogs.com/xiaojianliu/p/16131421.html
Copyright © 2020-2023  润新知