2.1、智能指针
std::make_unique
:智能指针的安全工厂
1.1 C++11的痛点
C++11引入了std::unique_ptr
,这是一个独占所有权的智能指针,极大简化了动态内存管理,避免了裸指针的内存泄漏风险。但unique_ptr
的创建仍需手动调用new
:
std::unique_ptr<Foo> p(new Foo(args));
这看似简单,但存在“异常安全”隐患:如果new Foo(args)
成功后,构造unique_ptr
的过程中抛异常,内存可能泄漏。此外,手写new
不符合现代C++“尽量不显式写new
”的设计理念。
1.2 C++14的改进
std::make_unique
是C++14新增的工厂函数模板,负责“安全地创建对象并返回unique_ptr
”,用法极简:
auto p = std::make_unique<Foo>(args);
它的优势:
- • 异常安全:内部先分配内存,再调用构造函数,若构造失败,自动释放内存,不会泄漏。
- • 代码简洁:消除
new
关键字,减少手写错误。 - • 统一风格:与
std::make_shared
对称,符合现代C++智能指针创建的最佳实践。
1.3底层原理
make_unique
本质上是一个模板函数,内部调用new
,然后用返回的裸指针构造unique_ptr
。但它将内存分配和智能指针构造封装在一个函数里,确保构造过程异常时,内存能被正确释放。它还支持完美转发构造参数,保证效率。
std::shared_timed_mutex
与std::shared_lock
:多线程读写锁的现代解法
2.1 C++11的多线程锁限制
C++11提供了std::mutex
、std::timed_mutex
等互斥锁,但它们都是独占锁,即同一时刻只有一个线程能持有锁。这在读多写少的场景下效率低,因为读操作也被串行化。
2.2 C++14的共享锁机制
C++14引入了std::shared_timed_mutex
,支持多读单写的锁策略:
- • 共享锁(shared ownership):允许多个线程同时持有共享锁,进行并发读操作。
- • 独占锁(exclusive ownership):写线程获得独占锁,阻止其他读写线程访问。
同时,C++14新增了std::shared_lock
,这是管理共享锁的RAII类,类似于std::unique_lock
管理独占锁。它自动在构造时获取共享锁,析构时释放锁,避免死锁和资源泄露。
2.3设计哲学与底层机制
std::shared_timed_mutex
内部维护一个计数器和状态,跟踪当前持有共享锁的线程数量和是否有独占锁。它支持带超时的锁请求(try_lock_for
等),方便实现响应式并发控制。
std::shared_lock
封装了共享锁的获取和释放,避免手动调用lock_shared
和unlock_shared
,降低出错风险。它的设计体现了现代C++对安全性、效率和易用性的统一追求。
三、深度案例:结合std::make_unique
与共享锁的线程安全缓存
下面的示例展示了如何用std::make_unique
安全创建资源,用std::shared_timed_mutex
和std::shared_lock
实现高效的多线程读写访问:
#include <iostream>
#include <memory>
#include <shared_mutex>
#include <thread>
#include <vector>
class ThreadSafeCache {
private:
std::unique_ptr<std::vector<int>> data_;
mutable std::shared_timed_mutex mutex_;
public:
ThreadSafeCache() : data_(std::make_unique<std::vector<int>>()) {}
// 写操作:独占锁保护
void add(int value) {
std::lock_guard<std::shared_timed_mutex> lock(mutex_);
data_->push_back(value);
std::cout << "Added: " << value << "\n";
}
// 读操作:共享锁保护
void print_all() const {
std::shared_lock<std::shared_timed_mutex> lock(mutex_);
std::cout << "Cache contents:";
for (int v : *data_) {
std::cout << " " << v;
}
std::cout << "\n";
}
};
int main() {
ThreadSafeCache cache;
std::thread writer([&cache]() {
for (int i = 0; i < 5; ++i) {
cache.add(i * 10);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
});
std::thread reader1([&cache]() {
for (int i = 0; i < 5; ++i) {
cache.print_all();
std::this_thread::sleep_for(std::chrono::milliseconds(30));
}
});
std::thread reader2([&cache]() {
for (int i = 0; i < 5; ++i) {
cache.print_all();
std::this_thread::sleep_for(std::chrono::milliseconds(40));
}
});
writer.join();
reader1.join();
reader2.join();
return 0;
}
案例解析
- •
std::make_unique
安全初始化:data_
用make_unique
创建,避免裸new
,保证异常安全和代码简洁。 - • 多线程同步:
mutex_
是std::shared_timed_mutex
,支持多读单写。 - • 写操作用
std::lock_guard
独占锁保护,保证写入时数据安全。 - • 读操作用
std::shared_lock
共享锁保护,允许多个线程并发读取,提升性能。 - • 线程调度模拟读写交替,体现共享锁的并发优势。
底层细节
- •
std::shared_timed_mutex
内部维护读者计数和写者标志,写线程独占时阻塞所有读线程,读线程共享时允许并发。 - •
std::shared_lock
构造时调用lock_shared()
,析构时调用unlock_shared()
,自动管理锁生命周期,防止死锁和资源泄露。 - •
std::lock_guard
则管理独占锁的生命周期,语义清晰。
四、常见误区与建议
- • 误用
new
创建unique_ptr
:手动写new
容易遗漏异常安全考虑,推荐始终用std::make_unique
。 - • 混用共享锁和独占锁不当:共享锁不能与独占锁同时存在,写操作必须独占,否则数据竞争。
- • 忘记加
mutable
导致锁对象无法修改:如果在Lambda
或类中使用锁,注意mutable
修饰符。 - • 滥用共享锁导致性能下降:共享锁内部实现复杂,频繁写操作或锁竞争严重时,可能不如简单互斥锁,需根据场景选择。
- • 忽视超时锁接口:
std::shared_timed_mutex
支持定时锁,合理使用可避免死锁和长时间阻塞。
五、总结
std::make_unique
和std::shared_timed_mutex
配合std::shared_lock
,是C++14对现代C++编程的两大进步。前者提升了智能指针的安全性和简洁性,后者则为多线程读写提供了高效、灵活的同步机制。理解它们的设计哲学--异常安全、资源管理自动化、并发性能优化--是写出健壮、高效现代C++代码的关键。通过实战案例,你不仅能掌握用法,更能洞悉底层实现,避免常见陷阱,真正做到“用得好、用得巧”。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)