从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(中);https://developer.aliyun.com/article/1522534
2. shared_ptr线程安全
智能指针复习:从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr-CSDN博客
以前敲的shared_ptr(加一个返回引用计数的接口):
namespace rtx { template<class T> class shared_ptr { public: shared_ptr(T* ptr = nullptr) : _ptr(ptr) , _pCount(new int(1)) {} void Release() { if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数 { delete _ptr; delete _pCount; } } ~shared_ptr() { Release(); } 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) if (_ptr != sp._ptr) // 防止自己给自己赋值,注意不能比较this,类似s1 = s2; 再来一次s1 = s2; { // 比较_pCount也行 //if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数 //{ // delete _ptr; // delete _pCount; //} Release(); _ptr = sp._ptr; _pCount = sp._pCount; (*_pCount)++; } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } int use_count() { return *_pCount; } protected: T* _ptr; int* _pCount;// 引用计数,有多线程安全问题,学了linux再讲,不能用静态成员 }; }
先看看库里面的使用:
int main() { std::shared_ptr<double> sp1(new double(7.77)); std::shared_ptr<double> sp2(sp1); mutex mtx; vector<thread> v(5); int n = 100000; for (auto& t : v) { t = thread([&](){ for (size_t i = 0; i < n; ++i) { // 拷贝是线程安全的 std::shared_ptr<double> sp(sp1); // 访问资源不是 (*sp)++; } }); } for (auto& t : v) { t.join(); } cout << *sp1 << endl; cout << sp1.use_count() << endl; return 0; }
2.1 库里面的shared_ptr使用
能指针共同管理的动态内存空间是线程不安全的,访问资源要自己加锁:
再把std换成自己的命名空间:
程序直接崩溃了,因为有时候引用计数不对。
多个线程及主线程中的所有智能指针都共享引用计数,又因为拷贝构造以及析构都不是原子的,所以导致线程不安全问题。
解决办法和Linux中一样,需要加锁:
引用计数加加和减减都要加锁
放个代码:
2.2 shared_ptr加锁代码
namespace rtx { template<class T> class shared_ptr { public: shared_ptr(T* ptr = nullptr) : _ptr(ptr) , _pCount(new int(1)) ,_pMtx(new mutex) {} shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr) , _pCount(sp._pCount) , _pMtx(sp._pMtx) { _pMtx->lock(); (*_pCount)++; _pMtx->unlock(); } shared_ptr<T>& operator=(const shared_ptr<T>& sp) { //if (this != &sp) if (_ptr != sp._ptr) // 防止自己给自己赋值,注意不能比较this,类似s1 = s2; 再来一次s1 = s2; { // 比较_pCount也行 //if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数 //{ // delete _ptr; // delete _pCount; //} Release(); _ptr = sp._ptr; _pCount = sp._pCount; _pMtx->lock(); (*_pCount)++; _pMtx->unlock(); } return *this; } void Release() // 防止产生内存泄漏,和析构一样,写成一个函数 { bool flag = false; _pMtx->lock(); if (--(*_pCount) == 0) { delete _ptr; delete _pCount; flag = true; } _pMtx->unlock(); if (flag) { delete _pMtx; // new出来的,引用计数为0时要delete } } ~shared_ptr() { Release(); } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } int use_count() { return *_pCount; } protected: T* _ptr; int* _pCount;// 引用计数,有多线程安全问题,学了linux再讲,不能用静态成员 mutex* _pMtx; }; } int main() { rtx::shared_ptr<double> sp1(new double(7.77)); rtx::shared_ptr<double> sp2(sp1); mutex mtx; vector<thread> v(7); int n = 100000; for (auto& t : v) { t = thread([&](){ for (size_t i = 0; i < n; ++i) { // 拷贝是线程安全的 rtx::shared_ptr<double> sp(sp1); // 访问资源不是 mtx.lock(); (*sp)++; mtx.unlock(); } }); } for (auto& t : v) { t.join(); } cout << *sp1 << endl; cout << sp1.use_count() << endl; return 0; }
3. 单例模式线程安全
单例模式复习:
从C语言到C++_37(特殊类设计和C++类型转换)单例模式-CSDN博客
3.1 懒汉模式线程安全问题
在C++11之后饿汉模式是没有线程安全问题的(做了相关补丁),因为单例对象是在main函数之前就实例化的,而多线程都是在main函数里面启动的。
但是懒汉模式是存在线程安全问题的,当多个线程使用到单例对象时候,在使用GetInstance()获取对象时,用因为调度问题出现误判,导致new多个单例对象。
这里给懒汉模式加个锁:(这里在getInstance这样加锁有没有什么问题?)
此时,每个调用GetInstance()的线程都需要申请锁然后释放锁,对锁的操作也是有开销的,会有效率上的损失。
单例模式在单例一经创建以后就不会再创建了,无论多少线程在访问已经创建的单例对象时都不会再创建,线程就已经安全了。所以在单例对象创建以后,根本没有必要再去申请锁和释放锁。
如果把加锁放在 if 里面呢?这样是不行的,因为第二次线程来的时候单例对象已经不是空的了,所以锁就白加了。
此时就要双检查加锁:
3.2 懒汉模式最终代码
class Singleton { public: static Singleton* GetInstance() { // 双检查加锁 if (m_pInstance == nullptr) // 保护第一次后,后续不需要加锁 { unique_lock<mutex> lock(_mtx); // 加锁,防止new抛异常就用unique_lock if (m_pInstance == nullptr) // 保护第一次时,线程安全 { m_pInstance = new Singleton; } } return m_pInstance; } private: Singleton() // 构造函数 {} Singleton(const Singleton& s) = delete; // 禁止拷贝 Singleton& operator=(const Singleton& s) = delete; // 禁止赋值 // 静态单例对象指针 static Singleton* m_pInstance; // 单例对象指针 static mutex _mtx; }; Singleton* Singleton::m_pInstance = nullptr; // 初始化为空 mutex Singleton::_mtx; int main() { Singleton* ps = Singleton::GetInstance();//获取单例对象 return 0; }
成功运行。
3.3 懒汉模式的另一种写法
放个代码:
class Singleton { public: static Singleton* GetInstance() { // 局部的静态对象,第一次调用时初始化 // 在C++11之前是不能保证线程安全的 // C++11之前局部静态对象的构造函数调用初始化并不能保证线程安全的原子性。 // C++11的时候修复了这个问题,所以这种写法,只能在支持C++11以后的编译器上使用 static Singleton _s; return &_s; } private: // 构造函数私有 Singleton() {}; Singleton(Singleton const&) = delete; Singleton& operator=(Singleton const&) = delete; }; int main() { Singleton::GetInstance(); return 0; }
C++11之前局部静态对象的构造函数调用初始化并不能保证线程安全的原子性。
C++11的时候修复了这个问题,所以这种写法,只能在支持C++11以后的编译器上使用。
本篇完。
应该算是本专栏的最后一篇了,泪目泪目。道阻且长,行则将至,想再深入学习C++以后就靠自己拓展了。后一部分就是网络和Linux网络的内容了。