规范参考总结了Qt、微软、谷歌和AUTOSAR C++14 等规范。
并发
目录
12. 并发编程规范
12.1 线程创建与管理规则
12.1.1 [必须] 明确定义线程的生命周期管理策略
- 无特殊需求时使用RAII技术管理线程资源,如使用
std::thread
或std::jthread
对象。 - 明确定义线程的启动、运行和终止条件,避免线程无限运行。
- 提供显式的线程启动和停止方法,而不是在构造函数或析构函数中管理线程。
- 在长时间运行的线程中实现可中断机制,定期检查中断标志。
- 在对象析构时确保所有关联线程安全终止。
示例:
class Worker {
std::jthread worker;
std::atomic<bool> stop_flag = false;
public:
Worker(std::function<void(std::atomic<bool>&)> task)
: worker([this, task](std::stop_token stoken) {
while (!stoken.stop_requested() && !stop_flag) {
task(stop_flag);
}
}) {}
~Worker() {
stop_flag = true;
worker.request_stop();
}
};
12.1.2 [必须] 为关键线程设置明确的标识符
- 为所有长期运行的关键线程设置有意义的名称。
- 临时线程或短期任务可以使用自动生成的标识符。
- 在日志和调试输出中包含线程标识符,以便于问题追踪。
示例:
#include <thread>
#include <string>
#include <sstream>
void setThreadName(const std::string& name) {
#ifdef __APPLE__
pthread_setname_np(name.c_str());
#elif defined(__linux__)
pthread_setname_np(pthread_self(), name.c_str());
#elif defined(_WIN32)
// Windows 的实现略有不同,这里省略
#endif
}
std::string getThreadName() {
char name[16] = {0}; // 大多数系统限制线程名为 16 字符
#ifdef __APPLE__
pthread_getname_np(pthread_self(), name, sizeof(name));
#elif defined(__linux__)
pthread_getname_np(pthread_self(), name, sizeof(name));
#elif defined(_WIN32)
// Windows 的实现略有不同,这里省略
#endif
return std::string(name);
}
void criticalTask() {
setThreadName("CriticalTask");
// 线程的主要工作...
log("Critical operation performed in thread: " + getThreadName());
}
void temporaryTask() {
std::stringstream ss;
ss << "TempTask-" << std::this_thread::get_id();
setThreadName(ss.str());
// 临时任务的工作...
log("Temporary task completed in thread: " + getThreadName());
}
int main() {
std::thread critical(criticalTask);
critical.join();
std::thread temp(temporaryTask);
temp.join();
return 0;
}
12.1.3 [必须] 在多线程环境中安全地处理异常
- 线程本地异常处理:
- 每个线程必须在其自身范围内捕获并处理异常。
- 禁止让异常跨越线程边界传播。
- 异常传递机制:
- 使用
std::promise
和std::future
在线程间传递异常信息。 - 对于线程池或长期运行的线程,实现自定义的异常传递机制。
- 使用
- 资源管理:
- 使用 RAII 技术确保即使在发生异常时资源也能被正确释放。
- 在异常处理程序中不要抛出新的异常;如果必须这样做,请使用
std::nested_exception
。
- 异常日志记录:
- 实现线程安全的日志机制,记录所有捕获的异常。
- 日志中包含异常类型、错误信息、线程ID和相关上下文信息。
- 致命错误处理:
- 对于无法恢复的致命错误,实现安全的程序终止机制。
- 使用
std::terminate_handler
自定义程序终止行为。
示例代码:
class ThreadSafeTask {
public:
void run() {
std::promise<void> promise;
std::future<void> future = promise.get_future();
std::thread worker([this, &promise]() {
try {
// 执行可能抛出异常的任务
doWork();
promise.set_value();
} catch (...) {
try {
// 捕获异常并通过 promise 传递
promise.set_exception(std::current_exception());
} catch (...) {
// 处理 set_exception 可能抛出的异常
std::terminate();
}
}
});
try {
// 等待任务完成或异常发生
future.get();
} catch (const std::exception& e) {
// 处理从工作线程传递的异常
logException("Worker thread exception", e);
// 可能的恢复逻辑
}
worker.join();
}
private:
void doWork() {
// 实际的工作逻辑
}
void logException(const std::string& context, const std::exception& e) {
// 线程安全的日志记录
std::lock_guard<std::mutex> lock(logMutex);
std::cerr << "Exception in " << context << ": " << e.what()
<< " (Thread ID: " << std::this_thread::get_id() << ")\n";
}
std::mutex logMutex;
};
12.2 线程同步规则
12.2.1 [必须] 使用线程安全的数据结构和访问模式
- 使用
std::atomic
进行简单的线程安全计数或标志。 - 对共享数据的访问进行封装,使用互斥锁或读写锁保护。
- 优先使用高级同步原语(如
std::shared_mutex
)而不是低级原语。
示例:
class ThreadSafeCounter {
mutable std::shared_mutex mutex_;
long value_ = 0;
public:
long increment() {
std::unique_lock lock(mutex_);
return ++value_;
}
long get() const {
std::shared_lock lock(mutex_);
return value_;
}
};
12.2.2 [必须] 使用RAII技术管理锁的生命周期
- 使用
std::lock_guard
、std::unique_lock
或std::scoped_lock
自动管理锁的生命周期。 - 尽量避免直接调用
mutex.lock()
和mutex.unlock()
,手动锁定和解锁操作应在确有必要时,经团队审查后使用。
示例:
std::mutex mutex1, mutex2;
{
std::scoped_lock lock(mutex1, mutex2);
// 临界区代码
} // 锁在此自动释放
12.2.3 [必须] 使用适当的粒度管理锁
- 最小化锁的作用范围,避免在持有锁时执行耗时操作。
- 使用读写锁(
std::shared_mutex
)区分读写操作,提高并发性。
示例:
class Cache {
mutable std::shared_mutex mutex_;
std::unordered_map<Key, Value> data_;
public:
std::optional<Value> get(const Key& key) const {
std::shared_lock lock(mutex_);
auto it = data_.find(key);
return it != data_.end() ? std::optional<Value>{it->second} : std::nullopt;
}
void set(const Key& key, const Value& value) {
std::unique_lock lock(mutex_);
data_[key] = value;
}
};
12.2.4 [必须] 正确使用条件变量避免虚假唤醒
- 使用条件变量的 wait 操作时,总是提供一个谓词(通常是 lambda 表达式)来检查等待条件。
- 确保在通知条件变量之前修改共享状态,并持有相关的互斥锁。
- 优先使用
wait
的重载版本,该版本接受一个谓词,而不是手动实现 while 循环。
示例:
std::mutex mutex;
std::condition_variable cv;
std::queue<int> work_queue;
void consumer() {
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [&]{ return !work_queue.empty(); });
int work = work_queue.front();
work_queue.pop();
lock.unlock();
process(work);
}
void producer(int item) {
std::lock_guard<std::mutex> lock(mutex);
work_queue.push(item);
cv.notify_one();
}
12.2.5 [必须] 实施一致的加锁顺序以预防死锁
- 在需要多个锁时,始终以相同的顺序获取锁。
- 使用
std::lock()
或std::scoped_lock
同时锁定多个互斥量。
示例:
void transfer(Account& from, Account& to, double amount) {
auto lock_both = [](Account& a1, Account& a2) {
std::scoped_lock lock(a1.mutex, a2.mutex);
};
if (&from < &to)
lock_both(from, to);
else
lock_both(to, from);
from.balance -= amount;
to.balance += amount;
}
12.2.6 [建议] 使用线程本地存储(TLS)避免共享状态冲突
- 在多线程环境中,对于每个线程都需要独立副本的数据,使用
thread_local
关键字声明。 - 适用场景:线程特定的计数器、缓存、随机数生成器等。
- 使用TLS可以避免不必要的同步开销,同时保证线程安全。
示例:
class ThreadSafeLogger {
static thread_local std::string thread_name;
static thread_local std::ostringstream log_buffer;
public:
static void setThreadName(const std::string& name) {
thread_name = name;
}
static void log(const std::string& message) {
log_buffer << "[" << thread_name << "] " << message << '\n';
}
static std::string flushLog() {
std::string result = log_buffer.str();
log_buffer.str(""); // Clear the buffer
return result;
}
};
// 使用示例
void workerThread(int id) {
ThreadSafeLogger::setThreadName("Worker-" + std::to_string(id));
ThreadSafeLogger::log("Starting work");
// ... 执行任务 ...
ThreadSafeLogger::log("Work completed");
std::cout << ThreadSafeLogger::flushLog();
}
12.2.7 [建议] 优先考虑使用原子操作代替低粒度锁,并根据场景选择适当的内存序
- 对于简单的共享状态(如标志、计数器),优先使用原子操作而非互斥锁。
- 对于 bool 类型的原子变量,可以考虑使用
memory_order_relaxed
,因为单个位的读写通常是原子的。 - 在性能关键路径上,可以考虑使用较弱的内存序,但必须通过压力测试验证正确性。
示例:
// 使用原子操作代替互斥锁
class AtomicFlag {
std::atomic<bool> flag_{false};
public:
void set() {
// 对于 bool,relaxed 通常足够,因为硬件保证了原子性
flag_.store(true, std::memory_order_relaxed);
}
bool test_and_set() {
// 这里使用 seq_cst 是为了确保完全的可见性和顺序
return flag_.exchange(true, std::memory_order_seq_cst);
}
void clear() {
flag_.store(false, std::memory_order_relaxed);
}
};
// 在性能关键路径上使用较弱的内存序
class LockFreeQueue {
std::atomic<Node*> head_{nullptr};
std::atomic<Node*> tail_{nullptr};
public:
void enqueue(T value) {
Node* new_node = new Node(std::move(value));
Node* prev_tail = tail_.exchange(new_node, std::memory_order_acq_rel);
if (prev_tail) {
prev_tail->next.store(new_node, std::memory_order_release);
} else {
head_.store(new_node, std::memory_order_release);
}
}
// dequeue 方法省略...
};
12.2.8 [建议] 避免忙等待,使用适当的同步机制
- 条件变量:
- 优先使用
std::condition_variable
代替简单的循环检查。 - 配合
std::mutex
使用,确保正确的同步。
- 优先使用
- 事件通知:
- 对于简单的一次性通知,考虑使用
std::promise
和std::future
对。
- 对于简单的一次性通知,考虑使用
- 信号量:
- 在需要限制并发访问资源数量时,使用
std::counting_semaphore
(C++20)或自定义信号量。
- 在需要限制并发访问资源数量时,使用
- 屏障同步:
- 对于需要多个线程同时达到某个点的情况,使用
std::barrier
(C++20)或自定义屏障。
- 对于需要多个线程同时达到某个点的情况,使用
- 超时机制:
- 使用带超时的等待函数,如
std::condition_variable::wait_for()
或std::future::wait_for()
。
- 使用带超时的等待函数,如
- 退避策略:
- 在确实需要轮询的场景,实现指数退避策略而不是持续的忙等待。
示例代码:
class ThreadSafeQueue {
std::queue<int> queue_;
mutable std::mutex mutex_;
std::condition_variable cv_;
public:
void push(int value) {
{
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(value);
}
cv_.notify_one();
}
bool try_pop(int& value, std::chrono::milliseconds timeout) {
std::unique_lock<std::mutex> lock(mutex_);
if (cv_.wait_for(lock, timeout, [this] { return !queue_.empty(); })) {
value = queue_.front();
queue_.pop();
return true;
}
return false;
}
};
// 使用示例
ThreadSafeQueue queue;
// 生产者线程
void producer() {
for (int i = 0; i < 10; ++i) {
queue.push(i);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
// 消费者线程
void consumer() {
int value;
while (true) {
if (queue.try_pop(value, std::chrono::seconds(1))) {
std::cout << "Consumed: " << value << std::endl;
} else {
std::cout << "Timeout, queue might be empty" << std::endl;
break;
}
}
}
// 使用 std::future 进行一次性通知
std::promise<void> ready_promise;
std::future<void> ready_future = ready_promise.get_future();
std::thread worker([&ready_future]() {
// 做一些准备工作
std::this_thread::sleep_for(std::chrono::seconds(2));
ready_future.wait(); // 等待主线程通知
// 开始实际工作
});
// 主线程
// ... 做一些其他工作 ...
ready_promise.set_value(); // 通知 worker 线程开始工作
worker.join();
12.3 并发优化策略
12.3.1 [建议] 数据量大时谨慎使用全局锁
-
使用分段锁(如
std::shared_mutex
)代替全局锁。分段锁:通过为数据结构的不同部分使用不同的锁,可以降低锁的争用程度,从而提高并行度。这对于大型数据结构特别有效,比如大型数组或哈希表。
-
在高并发场景中,考虑使用无锁算法或数据结构。
示例:
class ConcurrentHashMap {
static constexpr size_t NUM_BUCKETS = 101;
std::array<std::shared_mutex, NUM_BUCKETS> mutexes;
std::array<std::unordered_map<Key, Value>, NUM_BUCKETS> buckets;
size_t bucket(const Key& key) const {
return std::hash<Key>{}(key) % NUM_BUCKETS;
}
public:
std::optional<Value> get(const Key& key) const {
size_t b = bucket(key);
std::shared_lock lock(mutexes[b]);
auto it = buckets[b].find(key);
return it != buckets[b].end() ? std::optional<Value>{it->second} : std::nullopt;
}
void set(const Key& key, const Value& value) {
size_t b = bucket(key);
std::unique_lock lock(mutexes[b]);
buckets[b][key] = value;
}
};
12.3.2 [建议] 使用并行算法提高性能
- 在适当的场景中,使用C++17引入的并行算法。
- 使用
std::execution::par
或std::execution::par_unseq
策略来并行化算法。
示例:
std::vector<int> vec(10000);
std::iota(vec.begin(), vec.end(), 0);
// 并行排序
std::sort(std::execution::par, vec.begin(), vec.end());
// 并行查找
auto it = std::find(std::execution::par_unseq, vec.begin(), vec.end(), 42);
12.3.3 [建议] 合理应用原子操作以优化性能
- 原子操作应在无需完整锁定资源但需保持操作原子性的场景中使用,如单个变量的增减。
- 推荐使用
std::atomic
类型保证操作的原子性,避免引入重的同步机制。
结语
在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。
这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。
我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。
阅读我的优快云主页,解锁更多精彩内容:泡沫的优快云主页