ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-04-11 19:04:30  阅读:163  来源: 互联网

标签:std 生命周期 对象 线程 析构 shared ptr


线程安全的 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);
	}
}

标签:std,生命周期,对象,线程,析构,shared,ptr
来源: https://www.cnblogs.com/xiaojianliu/p/16131421.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有