1:开篇
本篇是 C++14 系列的收尾篇,将讲解 C++14 在并发编程 和字面量系统 两个领域的重要改进。这两个特性看似关联不大,却分别代表了 C++14 对高性能多线程编程 和代码可读性的终极优化。
std::shared_timed_mutex 填补了 C++11 标准库没有读写锁的空白,而用户定义字面量则让 C++ 的语法更加贴近自然语言。掌握这两个特性后,你将完整掌握现代 C++(C++11/C++14)的全部核心能力。
2:std::shared_timed_mutex与读写锁
1:基础语法
C++11 只提供了最基础的互斥锁 std::mutex,它采用独占访问模式 ------ 任何时刻只能有一个线程持有锁。对于读多写少的场景(如缓存、配置中心),独占锁会严重降低并发性能。
C++14 引入了 std::shared_timed_mutex,它支持两种访问模式:
- 共享模式(读锁):多个线程可以同时持有,用于只读操作
- 独占模式(写锁):任何时刻只能有一个线程持有,用于修改操作
cpp
#include <iostream>
#include <shared_mutex>
#include <mutex>
#include <thread>
#include <vector>
#include <chrono>
#include <syncstream> // C++20 同步输出,保证多线程输出不乱序
// 定义别名,方便后续切换互斥锁类型
using MutexType = std::shared_timed_mutex;
class ThreadSafeCounter {
private:
// mutable:允许在const成员函数中修改mutex
mutable MutexType mutex_;
unsigned int value_ = 0;
public:
ThreadSafeCounter() = default;
// 读操作:使用共享锁(shared_lock)
unsigned int get() const {
std::shared_lock<MutexType> lock(mutex_);
return value_;
}
// 写操作:使用独占锁(unique_lock)
void increment() {
std::unique_lock<MutexType> lock(mutex_);
++value_;
}
// 尝试获取独占锁(非阻塞)
bool try_increment() {
std::unique_lock<MutexType> lock(mutex_, std::try_to_lock);
if (lock.owns_lock()) {
++value_;
return true;
}
return false;
}
// 一段时间内尝试获取独占锁(带超时)
bool try_increment_for(int milliseconds) {
std::unique_lock<MutexType> lock(
mutex_, std::chrono::milliseconds(milliseconds));
return lock.owns_lock() ? (++value_, true) : false;
}
};
int main() {
ThreadSafeCounter counter;
const int N = 10;
// 写线程:每100ms递增一次
auto writer = [&counter]() {
for (int i = 0; i < N/2; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
counter.increment();
std::osyncstream(std::cout)
<< "写线程 " << std::this_thread::get_id()
<< ":计数器值 = " << counter.get() << "\n";
}
};
// 读线程:每50ms读取一次
auto reader = [&counter]() {
for (int i = 0; i < N; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::osyncstream(std::cout)
<< "读线程 " << std::this_thread::get_id()
<< ":读取到值 = " << counter.get() << "\n";
}
};
// 启动线程:1个写线程 + 3个读线程
std::vector<std::thread> threads;
threads.emplace_back(writer);
for (int i = 0; i < 3; ++i) {
threads.emplace_back(reader);
}
// 等待所有线程结束
for (auto& t : threads) {
t.join();
}
std::cout << "最终计数器值:" << counter.get() << "\n";
return 0;
}
2:读写锁原理和最佳实践
1:读写锁工作机制
读写锁的核心思想是 **"读写分离"**:
- 当没有线程持有写锁时,任意数量的线程都可以同时持有读锁
- 只要有一个线程持有写锁,其他所有线程(读和写)都必须等待
- 当有线程等待写锁时,新的读锁请求会被阻塞,避免写饥饿
注意:不同实现的读写锁调度策略不同。大多数实现(如 GCC 的 libstdc++)采用写优先策略,即写锁的优先级高于读锁,防止写线程长时间等待。
2:shared_timed_mutex和std::mutex对比
在读多写少的场景下,读写锁的性能优势非常明显:
| 场景 | std::mutex QPS |
std::shared_timed_mutex QPS |
性能提升 |
|---|---|---|---|
| 100% 读 | ~100 万 | ~1000 万 | 10 倍 |
| 99% 读 1% 写 | ~50 万 | ~500 万 | 10 倍 |
| 90% 读 10% 写 | ~10 万 | ~50 万 | 5 倍 |
| 50% 读 50% 写 | ~5 万 | ~3 万 | 反而下降 |
结论 :读写锁只适用于读操作远多于写操作的场景。如果读写比例接近 1:1,读写锁的性能会比普通互斥锁更差,因为它需要维护更复杂的状态。
3:std::shared_timed_mutex和C++17的std::shared_mutex
C++17 引入了更轻量的 std::shared_mutex,它和 shared_timed_mutex 的唯一区别是不支持超时锁定接口:
| 特性 | std::shared_timed_mutex(C++14) |
std::shared_mutex(C++17) |
|---|---|---|
| 共享锁 | ✅ lock_shared()/unlock_shared() |
✅ |
| 独占锁 | ✅ lock()/unlock() |
✅ |
| 非阻塞尝试锁 | ✅ try_lock()/try_lock_shared() |
✅ |
| 超时尝试锁 | ✅ try_lock_for()/try_lock_shared_for() |
❌ |
| 性能 | 较高 | 更高(无超时逻辑开销) |
选择原则:
- 如果不需要超时功能,且编译器支持 C++17,优先使用
std::shared_mutex - 如果需要超时控制,或者只能使用 C++14,使用
std::shared_timed_mutex
4:RAII锁的正确使用
C++14 同时提供了 std::shared_lock 用于管理共享锁,和 std::unique_lock 配合使用:
- 读操作:必须使用
std::shared_lock<MutexType> - 写操作:必须使用
std::unique_lock<MutexType>
错误用法:不要用
std::lock_guard管理读写锁,它不区分共享和独占模式。
3:常见陷阱和注意事项
1:锁升级导致死锁
cpp
// 致命错误:读锁升级为写锁会导致死锁
void bad_upgrade() {
std::shared_lock lock(mutex_); // 持有读锁
// 此时其他线程也持有读锁
std::unique_lock write_lock(mutex_); // 永远无法获取写锁,死锁!
}
解决方法:先释放读锁,再获取写锁;或者直接使用写锁。
2:写饥饿问题
虽然大多数实现采用写优先策略,但如果读线程非常密集,写线程仍然可能长时间等待。 解决方法:限制同时持有读锁的线程数量,或者使用带超时的写锁。
3:不要在持有锁期间执行耗时操作
无论是读锁还是写锁,持有锁的时间越长,并发性能越低。尽量将耗时操作放在锁外执行。
4:互斥锁的可重入性
std::shared_timed_mutex 和 std::mutex 一样,都是不可重入的。同一个线程不能重复获取同一个锁,否则会导致未定义行为。
3:用户定义字面量
1:基础语法
字面量后缀是附加在字面量后面的标识符,用于明确指定字面量的类型。C++98 已经支持一些内置后缀(如u、l、f),C++11 允许用户自定义后缀,C++14 则在标准库中提供了大量实用的标准后缀。
cpp
#include <iostream>
#include <string>
#include <string_view>
#include <chrono>
// 自定义字面量后缀:必须以下划线开头
// 1. 字符串字面量后缀
std::string operator "" _s(const char* str, size_t len) {
return std::string(str, len);
}
std::string_view operator "" _sv(const char* str, size_t len) {
return std::string_view(str, len);
}
// 2. 浮点数字面量后缀:公里转米
constexpr long double operator "" _km(unsigned long long x) {
return x * 1000.0L;
}
// 3. 浮点数字面量后缀:弧度转角度
constexpr long double operator "" _pi(long double x) {
return x * 3.14159265358979323846L;
}
int main() {
// 使用自定义后缀
auto str = "hello"_s; // std::string
auto sv = "world"_sv; // std::string_view
auto distance = 5_km; // 5000.0L
auto angle = 2.0_pi; // 6.283185307...
// C++14 标准库后缀(需要引入命名空间)
using namespace std::literals;
// 字符串字面量
auto std_str = "hello"s; // std::string
auto std_sv = "world"sv; // std::string_view(C++17)
// 时间字面量
auto hours = 24h; // std::chrono::hours
auto minutes = 30min; // std::chrono::minutes
auto seconds = 10s; // std::chrono::seconds
auto ms = 100ms; // std::chrono::milliseconds
// 使用时间字面量
std::this_thread::sleep_for(500ms); // 比 std::chrono::milliseconds(500) 简洁得多
return 0;
}
2:字面量系统的设计和应用
1:底层operator""重载
用户定义字面量的本质是重载特殊的运算符 operator"",不同类型的字面量对应不同的参数形式:
| 字面量类型 | 运算符签名 | 示例 |
|---|---|---|
| 整型 | ReturnType operator "" _suffix(unsigned long long); |
123_suffix |
| 浮点型 | ReturnType operator "" _suffix(long double); |
3.14_suffix |
| 字符 | ReturnType operator "" _suffix(char); |
'a'_suffix |
| 字符串 | ReturnType operator "" _suffix(const char*, size_t); |
"abc"_suffix |
强制规则:所有用户自定义后缀必须以下划线
_开头 。不以_开头的后缀保留给标准库使用(如s、h、ms)。
2:标准库提供的常用字面量
C++14 及后续版本在 std::literals 命名空间中提供了丰富的标准字面量:
| 后缀 | 类型 | 头文件 | 示例 |
|---|---|---|---|
s |
std::string |
<string> |
"hello"s |
sv |
std::string_view |
<string_view>(C++17) |
"hello"sv |
h |
std::chrono::hours |
<chrono> |
24h |
min |
std::chrono::minutes |
<chrono> |
30min |
s |
std::chrono::seconds |
<chrono> |
10s |
ms |
std::chrono::milliseconds |
<chrono> |
100ms |
us |
std::chrono::microseconds |
<chrono> |
100us |
ns |
std::chrono::nanoseconds |
<chrono> |
100ns |
i |
std::complex<double> |
<complex> |
2.0+3.0i |
注意 :标准库字面量不会自动导入,必须显式使用
using namespace std::literals;或者更具体的命名空间(如std::chrono_literals)。
3:编译期字面量
将 operator"" 声明为 constexpr,可以实现编译期字面量计算,所有计算都在编译时完成,运行时无开销:
cpp
// 编译期字节单位转换
constexpr unsigned long long operator "" _KB(unsigned long long x) {
return x * 1024;
}
constexpr unsigned long long operator "" _MB(unsigned long long x) {
return x * 1024_KB;
}
constexpr unsigned long long operator "" _GB(unsigned long long x) {
return x * 1024_MB;
}
int main() {
// 编译期计算:1GB = 1073741824字节
constexpr auto buffer_size = 1_GB;
static_assert(buffer_size == 1073741824ULL);
return 0;
}
4:应用
-
用于单位转换:这是自定义字面量最经典的应用场景,如长度单位(米、千米)、数据单位(KB、MB)、时间单位等
-
用于类型安全 :避免不同单位之间的隐式转换错误
cppstruct Meter { long double value; }; struct Kilometer { long double value; }; constexpr Meter operator "" _m(long double x) { return {x}; } constexpr Kilometer operator "" _km(long double x) { return {x}; } // 错误:不能直接相加,必须显式转换 // auto total = 1_m + 1_km; -
用于 DSL(领域特定语言):可以用字面量构建简洁的领域特定语言,如正则表达式、SQL 查询等
3:常见陷阱和注意事项
- 命名空间问题:标准库字面量必须显式导入命名空间,否则无法使用
- 后缀冲突:不同库可能定义相同的后缀,导致冲突。解决方法是使用命名空间隔离
- 字符串字面量的长度参数 :字符串字面量的
operator""必须接受两个参数(字符串指针和长度),不能只接受一个参数 - 不要过度使用:自定义字面量可以提高代码可读性,但过度使用会导致代码难以理解。只在能显著提升可读性的场景下使用。
4:本章总结
| 特性 | 解决的问题 | 核心优势 | 适用场景 |
|---|---|---|---|
std::shared_timed_mutex |
C++11 没有读写锁的空白 | 读多写少场景下的高并发 | 缓存、配置中心、只读数据结构 |
| 用户定义字面量 | 字面量类型不明确、可读性差 | 代码更接近自然语言、类型安全 | 单位转换、DSL、标准库类型快速创建 |
- 读多写少场景必须使用读写锁:不要用普通互斥锁替代,否则会浪费大量并发性能
- 永远使用 RAII 锁管理读写锁 :不要手动调用
lock()/unlock(),避免异常导致的死锁 - 优先使用标准库字面量 :
s、ms等标准后缀已经成为现代 C++ 的通用写法 - 自定义字面量只用于通用场景:不要为每个项目都定义大量自定义后缀
- 将 C++14 作为项目的最低标准:目前所有主流编译器都完全支持 C++14,没有理由再使用 C++11 或更早的版本
5:C++14全部特性总结
C++14 作为 C++11 的补丁版本,虽然没有引入颠覆性的新特性,但它填补了 C++11 的大量空白,让现代 C++ 真正变得可用、易用、高效。
| 类别 | 核心特性 | 解决的问题 |
|---|---|---|
| 语法改进 | 变量模板 | 消除类模板静态常量的冗余 |
| 泛型 Lambda | 简化局部泛型函数的写法 | |
| 函数返回类型推导 | 简化函数返回类型声明 | |
| 二进制字面量 | 提升位操作的可读性 | |
| 数字分隔符 | 让长数字更易读 | |
| 聚合类默认初始化 | 避免聚合类的未初始化错误 | |
| 标准库扩展 | std::exchange |
简化 "替换并返回旧值" 的操作 |
std::make_unique |
解决直接使用 new 的异常安全问题 | |
std::integer_sequence |
提供模板元编程的基础工具 | |
std::quoted |
简化带引号字符串的 I/O 操作 | |
std::shared_timed_mutex |
提供读写锁支持 | |
| 标准库字面量 | 提升代码可读性和类型安全 |
C++14 彻底巩固了 C++11 开创的现代 C++ 编程范式:
- 从手动管理资源到智能指针 :
std::make_unique的加入让std::unique_ptr成为默认的资源管理方式 - 从函数对象到 Lambda:泛型 Lambda 和初始化捕获让 Lambda 成为现代 C++ 中最常用的函数对象
- 从模板冗余到简洁表达:变量模板、返回类型推导大幅简化了模板代码的书写
- 从裸指针到安全抽象:标准库提供了越来越多的安全抽象,让开发者无需直接操作指针