C++ std::shared_ptr 的线程安全性
C++ std::shared_ptr 的线程安全性
一、std::shared_ptr 基础回顾
在深入探讨 std::shared_ptr
的线程安全性之前,先来回顾一下 std::shared_ptr
的基本概念和工作原理。std::shared_ptr
是 C++ 标准库提供的智能指针,用于管理动态分配的对象,它采用引用计数的方式来自动释放不再被使用的对象。
#include <memory>
#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructed" << std::endl; }
~MyClass() { std::cout << "MyClass destructed" << std::endl; }
};
int main() {
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr2 = ptr1;
std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl;
std::cout << "ptr2 use count: " << ptr2.use_count() << std::endl;
return 0;
}
在上述代码中,std::make_shared
创建了一个 MyClass
对象,并返回一个指向它的 std::shared_ptr
。然后将 ptr1
赋值给 ptr2
,这两个指针指向同一个对象,引用计数增加。use_count
函数用于获取当前对象的引用计数。当 ptr1
和 ptr2
离开作用域时,引用计数减为 0,MyClass
对象会自动被销毁。
二、线程安全问题的引入
在多线程环境下,对 std::shared_ptr
的操作可能会引发线程安全问题。因为多个线程同时访问和修改 std::shared_ptr
的引用计数以及所指向的对象,可能会导致数据竞争和未定义行为。
例如,考虑以下简单场景:有两个线程同时对一个 std::shared_ptr
进行操作,一个线程可能尝试释放对象(当引用计数变为 0 时),而另一个线程可能正在读取引用计数或者尝试增加引用计数。如果没有适当的同步机制,就可能出现以下问题:
- 数据竞争:两个线程同时修改引用计数,导致引用计数的值不一致,进而可能错误地提前释放对象或者未能及时释放对象。
- 双重释放:如果两个线程都认为自己是最后一个持有引用的线程,可能会导致对象被释放两次,这是严重的未定义行为。
三、std::shared_ptr 的线程安全保证
C++ 标准对 std::shared_ptr
的线程安全性有明确规定:
- 不同的
std::shared_ptr
对象:对不同的std::shared_ptr
对象(即指向不同对象的std::shared_ptr
)进行操作是线程安全的。这意味着如果两个线程分别操作不同的std::shared_ptr
,不需要额外的同步机制。
#include <memory>
#include <thread>
#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructed" << std::endl; }
~MyClass() { std::cout << "MyClass destructed" << std::endl; }
};
void threadFunction1() {
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
// 对 ptr1 进行各种操作,如访问成员函数等
}
void threadFunction2() {
std::shared_ptr<MyClass> ptr2 = std::make_shared<MyClass>();
// 对 ptr2 进行各种操作,如访问成员函数等
}
int main() {
std::thread t1(threadFunction1);
std::thread t2(threadFunction2);
t1.join();
t2.join();
return 0;
}
在上述代码中,threadFunction1
和 threadFunction2
分别操作不同的 std::shared_ptr
对象 ptr1
和 ptr2
,这种情况下是线程安全的,无需额外同步。
- 同一个
std::shared_ptr
对象:对同一个std::shared_ptr
对象的以下操作是线程安全的:- 读操作:例如
get()
获取原始指针、use_count()
获取引用计数、比较操作(如==
、!=
等)。这些操作不会修改std::shared_ptr
的内部状态,多个线程同时进行读操作不会引发数据竞争。
- 读操作:例如
#include <memory>
#include <thread>
#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructed" << std::endl; }
~MyClass() { std::cout << "MyClass destructed" << std::endl; }
};
void readSharedPtr(std::shared_ptr<MyClass>& ptr) {
std::cout << "Read use count: " << ptr.use_count() << std::endl;
MyClass* rawPtr = ptr.get();
// 可以安全地读取 rawPtr 指向的对象的 const 成员等
}
int main() {
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
std::thread t1(readSharedPtr, std::ref(ptr));
std::thread t2(readSharedPtr, std::ref(ptr));
t1.join();
t2.join();
return 0;
}
在这个例子中,多个线程同时调用 use_count()
和 get()
是线程安全的。
- **赋值操作**:将一个 `std::shared_ptr` 赋值给另一个 `std::shared_ptr` 是线程安全的。这包括 `operator=` 和 `reset()` 操作。赋值操作会自动处理引用计数的增减,并且保证在多线程环境下的原子性。
#include <memory>
#include <thread>
#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructed" << std::endl; }
~MyClass() { std::cout << "MyClass destructed" << std::endl; }
};
void assignSharedPtr(std::shared_ptr<MyClass>& dest, std::shared_ptr<MyClass>& src) {
dest = src;
}
int main() {
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr2;
std::thread t1(assignSharedPtr, std::ref(ptr2), std::ref(ptr1));
t1.join();
std::cout << "ptr2 use count: " << ptr2.use_count() << std::endl;
return 0;
}
这里,在多线程环境下将 ptr1
赋值给 ptr2
是线程安全的。
然而,对于同一个 std::shared_ptr
对象,除了上述安全的操作外,其他可能修改 std::shared_ptr
所指向对象状态的操作,如通过 *
或 ->
运算符访问对象的非 const
成员函数,不是线程安全的,需要额外的同步机制。
#include <memory>
#include <thread>
#include <iostream>
class MyClass {
public:
MyClass() : value(0) { std::cout << "MyClass constructed" << std::endl; }
void increment() { ++value; }
int getValue() const { return value; }
~MyClass() { std::cout << "MyClass destructed" << std::endl; }
private:
int value;
};
void modifySharedPtrObject(std::shared_ptr<MyClass>& ptr) {
ptr->increment();
}
int main() {
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
std::thread t1(modifySharedPtrObject, std::ref(ptr));
std::thread t2(modifySharedPtrObject, std::ref(ptr));
t1.join();
t2.join();
std::cout << "Final value: " << ptr->getValue() << std::endl;
return 0;
}
在上述代码中,modifySharedPtrObject
函数通过 std::shared_ptr
调用 increment
函数修改对象状态。如果多个线程同时执行这个函数,就会引发数据竞争,因为 increment
函数不是线程安全的,没有对 value
的修改进行同步。
四、使用同步机制确保线程安全
为了在多线程环境下安全地操作 std::shared_ptr
所指向对象的非线程安全成员,需要使用同步机制,如互斥锁(std::mutex
)、读写锁(std::shared_mutex
)等。
- 使用
std::mutex
:std::mutex
可以用来保护对std::shared_ptr
所指向对象的修改操作。
#include <memory>
#include <thread>
#include <iostream>
#include <mutex>
class MyClass {
public:
MyClass() : value(0) { std::cout << "MyClass constructed" << std::endl; }
void increment() {
std::lock_guard<std::mutex> lock(mutex);
++value;
}
int getValue() const {
std::lock_guard<std::mutex> lock(mutex);
return value;
}
~MyClass() { std::cout << "MyClass destructed" << std::endl; }
private:
int value;
mutable std::mutex mutex;
};
void modifySharedPtrObject(std::shared_ptr<MyClass>& ptr) {
ptr->increment();
}
int main() {
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
std::thread t1(modifySharedPtrObject, std::ref(ptr));
std::thread t2(modifySharedPtrObject, std::ref(ptr));
t1.join();
t2.join();
std::cout << "Final value: " << ptr->getValue() << std::endl;
return 0;
}
在 MyClass
类中,increment
和 getValue
函数使用 std::lock_guard<std::mutex>
来自动管理锁的获取和释放。这样,当多个线程调用这些函数时,就不会发生数据竞争。
- 使用
std::shared_mutex
:如果对std::shared_ptr
所指向对象的操作读多写少,可以使用std::shared_mutex
提高性能。std::shared_mutex
允许多个线程同时进行读操作,但只允许一个线程进行写操作。
#include <memory>
#include <thread>
#include <iostream>
#include <shared_mutex>
class MyClass {
public:
MyClass() : value(0) { std::cout << "MyClass constructed" << std::endl; }
void increment() {
std::unique_lock<std::shared_mutex> lock(mutex);
++value;
}
int getValue() const {
std::shared_lock<std::shared_mutex> lock(mutex);
return value;
}
~MyClass() { std::cout << "MyClass destructed" << std::endl; }
private:
int value;
mutable std::shared_mutex mutex;
};
void readSharedPtrObject(const std::shared_ptr<MyClass>& ptr) {
std::cout << "Read value: " << ptr->getValue() << std::endl;
}
void writeSharedPtrObject(std::shared_ptr<MyClass>& ptr) {
ptr->increment();
}
int main() {
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
std::thread t1(readSharedPtrObject, std::cref(ptr));
std::thread t2(readSharedPtrObject, std::cref(ptr));
std::thread t3(writeSharedPtrObject, std::ref(ptr));
t1.join();
t2.join();
t3.join();
std::cout << "Final value: " << ptr->getValue() << std::endl;
return 0;
}
在这个例子中,increment
函数使用 std::unique_lock<std::shared_mutex>
进行写操作,getValue
函数使用 std::shared_lock<std::shared_mutex>
进行读操作。多个读线程可以同时访问,而写线程会独占锁,确保数据的一致性。
五、弱指针与线程安全
std::weak_ptr
是与 std::shared_ptr
相关的一种智能指针,它不增加对象的引用计数,主要用于解决循环引用问题以及在对象可能已经被释放的情况下安全地获取指向对象的 std::shared_ptr
。
在多线程环境下,std::weak_ptr
的使用也需要注意线程安全。std::weak_ptr
的大部分操作与 std::shared_ptr
类似,对于不同的 std::weak_ptr
对象的操作是线程安全的,对同一个 std::weak_ptr
对象的读操作(如 expired()
判断对象是否已被释放、lock()
获取 std::shared_ptr
等)是线程安全的。
#include <memory>
#include <thread>
#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructed" << std::endl; }
~MyClass() { std::cout << "MyClass destructed" << std::endl; }
};
void checkWeakPtr(std::weak_ptr<MyClass>& weakPtr) {
std::shared_ptr<MyClass> sharedPtr = weakPtr.lock();
if (sharedPtr) {
std::cout << "Object is still alive" << std::endl;
} else {
std::cout << "Object has been destroyed" << std::endl;
}
}
int main() {
std::shared_ptr<MyClass> sharedPtr = std::make_shared<MyClass>();
std::weak_ptr<MyClass> weakPtr = sharedPtr;
std::thread t1(checkWeakPtr, std::ref(weakPtr));
std::thread t2(checkWeakPtr, std::ref(weakPtr));
t1.join();
t2.join();
return 0;
}
在上述代码中,多个线程同时调用 weakPtr.lock()
是线程安全的。但如果通过 lock()
获取到的 std::shared_ptr
去修改对象状态,同样需要额外的同步机制,因为对象状态的修改本身不是线程安全的。
六、总结 std::shared_ptr
线程安全要点
- 不同对象的
std::shared_ptr
:对不同的std::shared_ptr
对象(指向不同对象)的操作无需额外同步,是线程安全的。 - 同一对象的
std::shared_ptr
:- 读操作(
get()
、use_count()
、比较操作等)和赋值操作(operator=
、reset()
)是线程安全的。 - 通过
*
或->
运算符访问对象的非const
成员函数不是线程安全的,需要使用同步机制(如std::mutex
、std::shared_mutex
等)来保护。
- 读操作(
std::weak_ptr
:与std::shared_ptr
类似,不同对象的std::weak_ptr
操作线程安全,同一对象的std::weak_ptr
的读操作(expired()
、lock()
等)线程安全,但通过lock()
获取的std::shared_ptr
进行对象状态修改时仍需同步。
在多线程编程中,正确理解和处理 std::shared_ptr
的线程安全性是非常重要的,这可以避免数据竞争和未定义行为,确保程序的正确性和稳定性。通过合理使用同步机制,能够在充分利用 std::shared_ptr
便利性的同时,保障多线程环境下的程序安全。