1. 程式人生 > 其它 >執行緒安全的物件生命週期管理

執行緒安全的物件生命週期管理

執行緒安全的 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);
	}
}