裸指針與智能指針的線程安全問題

裸指針線程安全問題

使用普通裸指針造成的問題

#include <iostream>
#include <memory>
#include <thread>
using namespace std;
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	void funA()
	{
		cout << "A的一個非常好用的一個方法" << endl;
	}
};
void hander01(A *p)
{
    std::this_thread::sleep_for(std::chrono::seconds(2));
    p->funA();
}
int main()
{
    A *p = new A();
    thread t1(hander01,p);
    delete p;
    t1.join();
}

運行結果:

A()
~A()
A的一個非常好用的一個方法

A在析構完成之後還可以調用A的方法,這個操作是極其不安全的一個操作的,所以我們可以使用強弱智能指針來使得操作變得安全起來。

shared_ptr 和 weak_ptr的解決問題

#include <iostream>
#include <memory>
#include <thread>
using namespace std;
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	void funA()
	{
		cout << "A的一個非常好用的一個方法" << endl;
	}
};
void handler01(weak_ptr<A> q)
{
	//需要檢測對象A是否存活
	std::this_thread::sleep_for(std::chrono::seconds(2));
	shared_ptr<A> ptmp = q.lock();
	if (ptmp != nullptr)
	{
		ptmp->funA();
	}
	else
	{
		cout << "A對象已經析構" << endl;
	}
	
}
int main()
{
	{
		shared_ptr<A> p(new A());
		thread t1(handler01, weak_ptr<A>(p));
		t1.detach();
	}
	std::this_thread::sleep_for(std::chrono::seconds(10));
}

運行結果:

A()
~A()
A對象已經析構

shared_ptr的線程安全問題

智能指針shared_ptr本身(底層實現原理是引用計數)是線程安全的

智能指針的引用計數在手段上使用了atomic原子操作,只要shared_ptr在拷貝或賦值時增加引用,析構時減少引用就可以了。首先原子是線程安全的,所有智能指針在多線程下引用計數也是安全的

智能指針指向的對象的線程安全問題,智能指針沒有做任何保障

對於智能指針shared_ptr的引用計數本身是安全且無鎖的,但對象的讀寫則不是,因爲 shared_ptr 有兩個數據成員,一個是指向的對象的指針,還有一個就是我們上面看到的引用計數管理對象,當智能指針發生拷貝的時候,標準庫的實現是先拷貝智能指針,再拷貝引用計數對象(拷貝引用計數對象的時候,會使use_count加一),這兩個操作並不是原子操作,隱患就出現在這裏。

比如A線程在拷貝智能指針,因爲不是原子操作,恰好進行線程切換,導致沒有及時調用引用計數,另一個線程B把上一線程被拷貝的指針指向了新的智能指針,此操作把拷貝智能指針,再拷貝引用計數都做完了,那麼之前的引用計數減爲了0,指針是否.此時切換到線程A,開始調用引用計數,調用的就是已經切換後B指針的引用計數,但A指針的指向還是最初指針的懸掛指針。
如果還不明白,引用一下陳碩老師的例子:
地址點擊打開鏈接

多線程編程中的三個核心概念

(1)原子性的舉例

這一點,跟數據庫事務的原子性概念差不多,即一個操作(有可能包含有多個子操作)要麼全部執行(生效),要麼全部都不執行(都不生效)。

關於原子性,一個非常經典的例子就是銀行轉賬問題:比如A和B同時向C轉賬10萬元。如果轉賬操作不具有原子性,A在向C轉賬時,讀取了C的餘額爲20萬,然後加上轉賬的10萬,計算出此時應該有30萬,但還未來及將30萬寫回C的賬戶,此時B的轉賬請求過來了,B發現C的餘額爲20萬,然後將其加10萬並寫回。然後A的轉賬操作繼續——將30萬寫回C的餘額。這種情況下C的最終餘額爲30萬,而非預期的40萬。

(2)可見性的舉例

可見性是指,當多個線程併發訪問共享變量時,一個線程對共享變量的修改,其它線程能夠立即看到。可見性問題是好多人忽略或者理解錯誤的一點。

CPU從主內存中讀數據的效率相對來說不高,現在主流的計算機中,都有幾級緩存。每個線程讀取共享變量時,都會將該變量加載進其對應CPU的高速緩存裏,修改該變量後,CPU會立即更新該緩存,但並不一定會立即將其寫回主內存(實際上寫回主內存的時間不可預期)。此時其它線程(尤其是不在同一個CPU上執行的線程)訪問該變量時,從主內存中讀到的就是舊的數據,而非第一個線程更新後的數據。

這一點是操作系統或者說是硬件層面的機制,所以很多應用開發人員經常會忽略。

(3)順序性舉例

順序性指的是,程序執行的順序按照代碼的先後順序執行。處理器爲了提高程序整體的執行效率,可能會對代碼進行優化,其中的一項優化方式就是調整代碼順序,按照更高效的順序執行代碼。講到這裏,有人要着急了——什麼,CPU不按照我的代碼順序執行代碼,那怎麼保證得到我們想要的效果呢?實際上,大家大可放心,CPU雖然並不保證完全按照代碼順序執行,但它會保證程序最終的執行結果和代碼順序執行時的結果一致。

總結

如果你能保證不會有多個線程同時修改或替換指針指向的對象,不用加鎖是完全沒有問題的。
如果希望在多個線程使用同一個對象的智能指針,可以讓每個線程使用這個指針的不同副本或者使用鎖保護這個指針。

解決辦法-加入鎖機制

使用互斥鎖對多線程讀寫同一個shared_ptr進行加鎖操作(多個線程訪問同一資源時,爲了保證數據的一致性,最簡單的方式就是使用 mutex(互斥鎖))

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章