C++ — 智能指針二

shared_ptr

shared_ptr的原理: 是通過引用計數的方式來實現多個shared_ptr對象之間共享資源。

  1. shared_ptr在其內部,給每個資源都維護了着一份計數,用來記錄該份資源被幾個對象共享。
  2. 對象被銷燬時(也就是析構函數調用),就說明自己不使用該資源了,對象的引用計數減一
  3. 如果引用計數是0,就說明自己是最後一個使用該資源的對象,必須釋放該資源
  4. 如果不是0,就說明除了自己還有其他對象在使用該份資源,不能釋放該資源,否則其他對象就成野指針了。
//實現了引用計數
//引用++,析構--,到1時釋放
template<class T>
class shared_ptr
{
public:
    shared_ptr(T* ptr)
        :_ptr(ptr)
            ,_pcount(new int(1))    //1爲缺省值
        {}
    shared_ptr(const shared_ptr<T>& sp) 
        :_ptr(sp._ptr)
            ,_pcount(sp._pcount)
    {
        (*pcount)++;
    }
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        //if(this != &sp)    //sp1與sp2管理同一塊空間時,sp1 = sp2時,會--後又++
        if(_ptr != sp._ptr)    //同一塊空間使用同一個引用計數
        {
            if(--(*_pcount ) == 0)
            {
                 //開始使用新空間,需要釋放舊空間的引用計數,但是原來的空間是公用的,所以需要判斷
                delete _ptr;
                delete _pcount;   
            }
            _ptr = sp._ptr;
            _pcount = sp._pcount;
            *(_pcount)++;
        }
        return *this;
    }
    ~shared_ptr()
    {
        if(--(*_pcount ) == 0)
        {
            delete _ptr;    
            delete _pcount;
            _pcount = nullptr;
        }
    }
private:
    T* _ptr;
    static int* _pcount;    //防止出現多個引用計數
    //多個空間,_pcount只有一個,所以每使用一塊新空間就就開闢一塊空間存放
    //_pcount並不保證線程安全,但是互斥鎖是有缺陷的,互斥鎖適合力度大的,自旋鎖適合力度小的
};

shared_ptr的線程安全問題

shared_ptr的線程安全分爲兩方面

  1. 智能指針對象中引用計數是多個智能指針對象共享的,兩個線程中智能指針的引用計數同時++或–,這個操作不是原子的.
  2. 智能指針管理的對象存放在堆上,兩個線程中同時去訪問,會導致線程安全問題。
//線程安全問題演示
namespace my
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new int(1))    //1爲缺省值
			, _pmutex(new std::mutex)
		{}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			AddRefCount();
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if(this != &sp)    //sp1與sp2管理同一塊空間時,sp1 = sp2時,會--後又++
			if (_ptr != sp._ptr)    //同一塊空間使用同一個引用計數
			{
				Release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pmutex = sp._pmutex;
				AddRefCount()
			}
			return *this;
		}
		~shared_ptr()
		{
			Release();
		}
		void AddRefCount()	//在需要count++的位置調用
		{
			// 加鎖或者使用加1的原子操作
			_pmutex->lock();
			++(*_pRefCount);
			_pmutex->unlock();
		}
		void Release()	//完成   釋放
		{
			bool deleteflag = false;
			_pMutex.lock();
            //這裏最好使用RAII設計的互斥鎖更加安全
            //unique_lock<mutex> lock(m_mtx);
			if (--(*_pRefCount) == 0)
			{
				//開始使用新空間,需要釋放舊空間的引用計數,但是原來的空間是公用的,所以需要判斷
				delete _ptr;
				delete _pcount;
				//delete _pmutex;	//不能提前釋放鎖,因爲還沒有解鎖
				_pcount = nullptr;
				_ptr = nullptr;
				
				deleteflag = true;
			}
			_pMutex.unlock();
			if (deleteflag == true)
			{
				delete _pMutex;
				_pmutex = nullptr;
			}
				
		}
		T* Get_ptr()
		{
			return _ptr;
		}
		int Get_count()
		{
			return *_pcount;
		}
	private:
		T* _ptr;
		static int* _pcount;    //防止出現多個引用計數
		//多個空間,_pcount只有一個,所以每使用一塊新空間就就開闢一塊空間存放
		//_pcount並不保證線程安全,但是互斥鎖是有缺陷的,互斥鎖適合力度大的,自旋鎖適合力度小的
		std::mutex* _pmutex; // 互斥鎖,管理同一個鎖	
	};
	struct Date
	{
		int _year;
		int _month;
		int _day;
		~Date()
		{}
	};
};

void SharePtrFunc(my::shared_ptr<my::Date>& sp, size_t n)
{
	std::cout << sp.Get_count() << std::endl;
	for (size_t i = 0; i < n; ++i)	//使用p這個智能指針拷貝
	{
		// 這裏智能指針拷貝會++計數,智能指針析構會--計數,這裏是線程安全的。
		my::shared_ptr<my::Date> copy(sp);
		// 這裏智能指針訪問管理的資源,不是線程安全的。所以我們看看這些值兩個線程++了2n次,但是最終看到的結果,並一定是加了2n
        //這裏sharet_ptr是安全的,但是Date類中的資源卻不是安全的,可能多個線程同時取使用
		/*copy->_year++;    
		copy->_month++;
		copy->_day++;*/
	}
}
//正常調用Date的析構函數則無錯
int main()
{
	my::shared_ptr<my::Date> p(new my::Date);	//創建Date類並用智能指針p管理
	std::cout << p.Get_count() << std::endl;
	const size_t n = 1000;	//數值=10時正確調用,等於1000則未正常調用,說明多線程是有概率問題的,需要考慮線程安全的問題
	//加入線程安全
	std::thread t1(SharePtrFunc, p, n);	//頭文件
	std::thread t2(SharePtrFunc, p, n);
	t1.join();
	t2.join();

	return 0;
}

shared_ptr本身是線程安全的,但是管理的資源並不是安全的,
成員爲類對象時,需要使用->去訪問

shared_ptr的循環引用

struct ListNode
{
	int _data;
	std::shared_ptr<ListNode> _prev;
	std::shared_ptr<ListNode> _next;
	~ListNode(){ std::cout << "~ListNode()" << std::endl; }
};
int main()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);
	std::cout << node1.use_count() << std::endl;
	std::cout << node2.use_count() << std::endl;
	//兩行同時應用則無法正確釋放
	node1->_next = node2;
	node2->_prev = node1;
	std::cout << node1.use_count() << std::endl;    //獲取shared_ptr的引用計數
	std::cout << node2.use_count() << std::endl;
	return 0;
}
//循環引用分析:
//1. node1和node2兩個智能指針對象指向兩個節點,引用計數變成1,我們不需要手動delete。
//2. node1的_next指向node2,node2的_prev指向node1,引用計數變成2。
//3. node1和node2析構,引用計數減到1,但是_next還指向下一個節點。但是_prev還指向上一個節點。
//4. 也就是說_next析構了,node2就釋放了。
//5. 也就是說_prev析構了,node1就釋放了。
//6. 但是_next屬於node的成員,node1釋放了,_next纔會析構,而node1由_prev管理,_prev屬於node2成員,所以這就叫循環引用,誰也不會釋放


//解決方案:在引用計數的場景下,把節點中的_prev和_next改成 弱指針weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;時  weak_ptr的_next和_prev不會增加node1和node2的引用計數。
struct ListNode
{
	int _data;
	std::weak_ptr<ListNode> _prev;
	std::weak_ptr<ListNode> _next;
	~ListNode(){ std::cout << "~ListNode()" << std::endl; 
};
//weak_ptr是專門爲解決shared_ptr的循環引用而解決的,不會增加引用計數,但是可以將shared_ptr的值賦給weak_ptr.
  1. 智能指針不一定能管理malloc出來的空間,因爲智能指針管理的這一塊空間並沒有初始化,這塊空間中如果沒有指針,釋放的時候是去調用析構函數delete,本質上是使用free,不需要考慮指針的問題。
    但是如果這塊空間中有指針(如string),這個指針也指向一塊空間,使用malloc而沒有調用構造函數,指針爲隨機值,結束後調用析構函數delete時會釋放這個指針,而這個指針是隨機值,則出錯。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章