网站本地可以打开,logo设计免费平台,下好的字体怎么导入wordpress,全球采购商平台文章目录 共享数据的问题3.1.1 条件竞争双链表的例子条件竞争示例恶性条件竞争的特点 3.1.2 避免恶性条件竞争1. 使用互斥量保护共享数据结构2. 无锁编程3. 软件事务内存#xff08;STM#xff09; 总结互斥量与共享数据保护3.2.1 互斥量使用互斥量保护共享数据示例代码… 文章目录 共享数据的问题3.1.1 条件竞争双链表的例子条件竞争示例恶性条件竞争的特点 3.1.2 避免恶性条件竞争1. 使用互斥量保护共享数据结构2. 无锁编程3. 软件事务内存STM 总结互斥量与共享数据保护3.2.1 互斥量使用互斥量保护共享数据示例代码 C17的新特性面向对象设计中的互斥量 3.2.2 保护共享数据示例代码 解决方案 3.2.3 接口间的条件竞争示例代码解决方案 总结接口间的条件竞争与解决方案3.2.3 接口间的条件竞争示例std::stack 容器的实现解决方案重新设计接口示例线程安全的堆栈类定义 3.2.4 死锁问题描述及解决方案示例使用 std::lock 和 std::lock_guard使用 std::scoped_lockC17总结 3.2.5 避免死锁的进阶指导死锁的原因与常见场景避免嵌套锁避免在持有锁时调用外部代码使用固定顺序获取锁使用层次锁结构示例使用层次锁来避免死锁超越锁的延伸扩展使用 std::unique_lock 提供灵活性示例使用 std::unique_lock 和 std::defer_lock 不同域中互斥量的传递 总结3.2.8 锁的粒度锁的粒度简介类比超市结账场景细粒度锁 vs 粗粒度锁示例优化锁的使用控制锁的持有时间示例细粒度锁的应用条件竞争与语义一致性寻找合适的机制 总结3.3 保护共享数据的方式3.3.1 保护共享数据的初始化过程单线程延迟初始化多线程延迟初始化双重检查锁模式使用 std::call_once 和 std::once_flag静态局部变量的线程安全初始化 3.3.2 保护不常更新的数据结构使用 std::shared_mutex 3.3.3 嵌套锁使用 std::recursive_mutex 总结 共享数据的问题
3.1.1 条件竞争
在多线程编程中共享数据的修改是导致问题的主要原因。如果数据只读则不会影响数据的一致性所有线程都能获得相同的数据。然而当一个或多个线程需要修改共享数据时就会出现许多复杂的问题。这些问题通常涉及**不变量invariants**的概念即描述特定数据结构的某些属性例如“变量包含列表中的项数”。更新操作通常会破坏这些不变量特别是在处理复杂数据结构时。
双链表的例子
以双链表为例每个节点都有指向前一个节点和后一个节点的指针。为了从列表中删除一个节点必须更新其前后节点的指针这会导致不变量暂时被破坏
找到要删除的节点N更新前一个节点指向N的指针让其指向N的下一个节点更新后一个节点指向N的指针让其指向前一个节点删除节点N
在这过程中步骤2和步骤3之间不变量被破坏因为此时部分指针已经更新但还未完全完成。如果其他线程在此期间访问该链表可能会读取到不一致的状态从而导致程序错误甚至崩溃。这种问题被称为条件竞争race condition。
条件竞争示例
假设你去一家大电影院买电影票有多个收银台可以同时售票。当另一个收银台也在卖你想看的电影票时你的座位选择取决于之前已预定的座位。如果有少量座位剩余可能会出现一场抢票比赛看谁能抢到最后的票。这就是一个典型的条件竞争例子你的座位或电影票取决于购买的顺序。
在并发编程中条件竞争取决于多个线程的执行顺序。大多数情况下即使改变执行顺序结果仍然是可接受的。然而当不变量遭到破坏时条件竞争就可能变成恶性竞争例如在双链表的例子中可能导致数据结构永久损坏并使程序崩溃。
C标准定义了**数据竞争data race**这一术语指的是并发修改独立对象的情况这种情况会导致未定义行为。
恶性条件竞争的特点
难以查找和复现由于问题出现的概率较低且依赖于特定的执行顺序因此很难查找和复现。时间敏感调试模式下程序的执行速度变慢错误可能完全消失因为调试模式会影响程序的执行时间。负载敏感随着系统负载增加执行序列问题复现的概率也会增加。
3.1.2 避免恶性条件竞争
为了避免恶性条件竞争以下是几种常见的解决方案
1. 使用互斥量保护共享数据结构
最简单的方法是对共享数据结构使用某种保护机制确保只有修改线程才能看到不变量的中间状态。C标准库提供了多种互斥量如 std::mutex可以用来保护共享数据结构确保只有一个线程能进行修改其他线程要么等待修改完成要么读取到一致的数据。
2. 无锁编程
另一种方法是对数据结构和不变量进行设计使其能够完成一系列不可分割的变化保证每个不变量的状态。这种方法称为无锁编程虽然高效但实现难度较大容易出错。
3. 软件事务内存STM
还有一种处理条件竞争的方式是使用事务的方式处理数据结构的更新类似于数据库中的事务管理。所需的数据和读取操作存储在事务日志中然后将之前的操作进行合并并提交。如果数据结构被另一个线程修改提交操作将失败并重新尝试。这种方法称为软件事务内存Software Transactional Memory, STM是一个热门的研究领域但在C标准中没有直接支持。
总结
共享数据问题当多个线程共享数据时特别是当数据需要被修改时会出现条件竞争问题。不变量描述数据结构的某些属性在修改过程中可能会被破坏。条件竞争多个线程争夺对共享资源的访问权可能导致程序错误或崩溃。避免恶性条件竞争的方法 互斥量使用互斥量保护共享数据结构确保只有一个线程能进行修改。无锁编程设计数据结构使其能完成一系列不可分割的变化。软件事务内存STM使用事务的方式处理数据结构的更新确保一致性。
通过上述方法开发者可以有效避免多线程编程中的条件竞争问题确保程序的正确性和稳定性。
互斥量与共享数据保护
3.2.1 互斥量
使用互斥量保护共享数据
在多线程环境中使用互斥量std::mutex可以确保对共享数据的访问是互斥的从而避免条件竞争问题。C标准库提供了std::lock_guard它利用RAII机制自动管理互斥量的锁定和解锁。
示例代码
#include list
#include mutex
#include algorithmstd::listint some_list; // 1: 全局变量
std::mutex some_mutex; // 2: 全局互斥量void add_to_list(int new_value)
{std::lock_guardstd::mutex guard(some_mutex); // 3: 锁定互斥量some_list.push_back(new_value);
}bool list_contains(int value_to_find)
{std::lock_guardstd::mutex guard(some_mutex); // 4: 锁定互斥量return std::find(some_list.begin(), some_list.end(), value_to_find) ! some_list.end();
}全局变量与互斥量some_list是一个全局变量被一个全局互斥量some_mutex保护。std::lock_guard在add_to_list和list_contains函数中使用std::lock_guard来自动管理互斥量的锁定和解锁确保在函数执行期间互斥量处于锁定状态防止其他线程访问共享数据。
C17的新特性
C17引入了模板类参数推导简化了std::lock_guard的使用
std::lock_guard guard(some_mutex); // 模板参数类型由编译器推导此外C17还引入了std::scoped_lock提供了更强大的功能
std::scoped_lock guard(some_mutex);为了兼容C11标准本文将继续使用带有模板参数类型的std::lock_guard。
面向对象设计中的互斥量
将互斥量与需要保护的数据放在同一个类中可以使代码更加清晰并且方便了解什么时候对互斥量上锁。例如
class ProtectedData {
private:std::listint data;std::mutex mutex;public:void add_to_list(int new_value) {std::lock_guardstd::mutex guard(mutex);data.push_back(new_value);}bool contains(int value_to_find) {std::lock_guardstd::mutex guard(mutex);return std::find(data.begin(), data.end(), value_to_find) ! data.end();}
};这种设计方式不仅封装了数据还确保了所有对共享数据的访问都在互斥量保护下进行。
3.2.2 保护共享数据
使用互斥量保护数据不仅仅是简单地在每个成员函数中加入一个std::lock_guard对象。必须注意以下几点 避免返回指向受保护数据的指针或引用 如果成员函数返回指向受保护数据的指针或引用外部代码可以直接访问这些数据而无需通过互斥量保护这会破坏数据保护机制。 检查成员函数是否通过指针或引用来调用 尤其是在调用不在你控制下的函数时确保这些函数不会存储指向受保护数据的指针或引用。
示例代码
class SomeData {int a;std::string b;
public:void do_something();
};class DataWrapper {
private:SomeData data;std::mutex m;public:templatetypename Functionvoid process_data(Function func) {std::lock_guardstd::mutex l(m);func(data); // 传递“保护”数据给用户函数}
};SomeData* unprotected;void malicious_function(SomeData protected_data) {unprotected protected_data;
}DataWrapper x;void foo() {x.process_data(malicious_function); // 传递恶意函数unprotected-do_something(); // 在无保护的情况下访问保护数据
}在这个例子中尽管process_data函数内部使用了互斥量保护数据但传递给用户的函数func可能会绕过保护机制导致数据被不安全地访问。
解决方案
不要将受保护数据的指针或引用传递到互斥锁作用域之外。确保所有对受保护数据的访问都在互斥量保护下进行。
3.2.3 接口间的条件竞争
即使使用了互斥量保护数据如果接口设计不当仍然可能存在条件竞争。例如如果某个接口允许返回指向受保护数据的指针或引用外部代码可以在没有互斥量保护的情况下访问这些数据导致数据不一致。
示例代码
class ProtectedData {
private:std::listint data;std::mutex mutex;public:const std::listint get_data() { // 返回引用可能导致条件竞争std::lock_guardstd::mutex guard(mutex);return data;}
};在这种情况下虽然get_data函数内部使用了互斥量保护数据但返回的引用可以在互斥量保护范围之外被访问从而导致潜在的条件竞争。
解决方案
避免返回指向受保护数据的指针或引用除非这些指针或引用本身也在互斥量保护下使用。设计接口时确保所有对受保护数据的访问都在互斥量保护范围内。
总结
互斥量的作用互斥量用于保护共享数据确保同一时间只有一个线程能够访问和修改数据从而避免条件竞争。std::lock_guard利用RAII机制自动管理互斥量的锁定和解锁简化了代码编写。面向对象设计中的互斥量将互斥量与需要保护的数据放在同一个类中使得代码更加清晰并便于管理。避免返回指针或引用确保所有对受保护数据的访问都在互斥量保护下进行避免返回指向受保护数据的指针或引用。接口设计注意事项确保接口设计合理避免通过接口泄露受保护数据的指针或引用防止条件竞争的发生。
通过正确使用互斥量和精心设计接口开发者可以有效避免多线程编程中的条件竞争问题确保程序的正确性和稳定性。
接口间的条件竞争与解决方案
3.2.3 接口间的条件竞争
即使使用了互斥量或其他机制保护共享数据仍然需要确保数据是否真正受到了保护。例如在双链表的例子中为了线程安全地删除一个节点不仅需要保护待删除节点及其前后相邻的节点还需要保护整个删除操作的过程。最简单的解决方案是使用互斥量来保护整个链表或数据结构。
示例std::stack 容器的实现
考虑一个类似于 std::stack 的栈类
templatetypename T, typename Container std::dequeT
class stack {
public:explicit stack(const Container);explicit stack(Container Container());template class Alloc explicit stack(const Alloc);template class Alloc stack(const Container, const Alloc);template class Alloc stack(Container, const Alloc);template class Alloc stack(stack, const Alloc);bool empty() const;size_t size() const;T top();T const top() const;void push(T const);void push(T);void pop();void swap(stack);template class... Args void emplace(Args... args); // C14的新特性
};尽管每个成员函数都可能在内部使用互斥量保护数据但接口设计上的问题仍可能导致条件竞争。例如
empty() 和 size()虽然这些函数在返回时可能是正确的但在返回后其他线程可能会修改栈的内容导致之前的结果变得不可靠。top() 和 pop()如果两个线程分别调用 top() 和 pop()可能会出现竞态条件因为在这两个操作之间另一个线程可能会修改栈的状态。
解决方案重新设计接口
为了避免上述问题可以通过重新设计接口来解决条件竞争 选项1传入引用获取弹出值 std::vectorint result;
some_stack.pop(result);缺点 需要构造一个目标类型的实例这可能不现实或资源开销大。不适用于所有类型特别是那些没有赋值操作的类型。 选项2无异常抛出的拷贝构造函数或移动构造函数 使用无异常抛出的拷贝构造函数或移动构造函数可以避免某些异常问题但这限制了可使用的类型范围。 选项3返回指向弹出值的指针 返回一个指向弹出元素的指针如 std::shared_ptr可以避免内存分配问题并且不会抛出异常。 std::shared_ptrT pop() {std::lock_guardstd::mutex lock(m);if (data.empty()) throw empty_stack();std::shared_ptrT res(std::make_sharedT(data.top()));data.pop();return res;
}选项4结合选项1和选项3 提供多个接口选项让用户选择最适合的方案。
示例线程安全的堆栈类定义
以下是一个线程安全的堆栈类定义示例结合了选项1和选项3
#include exception
#include memory
#include mutex
#include stackstruct empty_stack : std::exception {const char* what() const throw() {return empty stack!;}
};templatetypename T
class threadsafe_stack {
private:std::stackT data;mutable std::mutex m;public:threadsafe_stack() : data(std::stackT()) {}threadsafe_stack(const threadsafe_stack other) {std::lock_guardstd::mutex lock(other.m);data other.data; // 在构造函数体中的执行拷贝}threadsafe_stack operator(const threadsafe_stack) delete;void push(T new_value) {std::lock_guardstd::mutex lock(m);data.push(new_value);}std::shared_ptrT pop() {std::lock_guardstd::mutex lock(m);if (data.empty()) throw empty_stack();std::shared_ptrT res(std::make_sharedT(data.top()));data.pop();return res;}void pop(T value) {std::lock_guardstd::mutex lock(m);if (data.empty()) throw empty_stack();value data.top();data.pop();}bool empty() const {std::lock_guardstd::mutex lock(m);return data.empty();}
};3.2.4 死锁问题描述及解决方案
死锁是指两个或多个线程互相等待对方释放资源导致所有线程都无法继续执行的情况。例如
线程A持有互斥量A并请求互斥量B。线程B持有互斥量B并请求互斥量A。
为了避免死锁可以采取以下措施
保持一致的加锁顺序确保所有线程以相同的顺序获取互斥量。使用 std::lock 或 std::scoped_lockC标准库提供了 std::lock 和 std::scoped_lock可以一次性锁住多个互斥量避免死锁。
示例使用 std::lock 和 std::lock_guard
class some_big_object;
void swap(some_big_object lhs, some_big_object rhs);class X {
private:some_big_object some_detail;std::mutex m;public:X(some_big_object const sd) : some_detail(sd) {}friend void swap(X lhs, X rhs) {if (lhs rhs)return;std::lock(lhs.m, rhs.m); // 1 锁住两个互斥量std::lock_guardstd::mutex lock_a(lhs.m, std::adopt_lock); // 2std::lock_guardstd::mutex lock_b(rhs.m, std::adopt_lock); // 3swap(lhs.some_detail, rhs.some_detail);}
};使用 std::scoped_lockC17
C17引入了 std::scoped_lock可以简化多互斥量锁定的代码
void swap(X lhs, X rhs) {if (lhs rhs)return;std::scoped_lock guard(lhs.m, rhs.m); // 1 自动推导模板参数swap(lhs.some_detail, rhs.some_detail);
}总结
条件竞争即使使用互斥量保护共享数据接口设计不当仍可能导致条件竞争。通过重新设计接口可以有效避免这些问题。死锁避免死锁的关键在于保持一致的加锁顺序或使用 std::lock 和 std::scoped_lock 来一次性锁住多个互斥量。接口设计建议 避免返回指向受保护数据的指针或引用。尽量减少不必要的接口复杂性确保所有对共享数据的访问都在互斥量保护下进行。使用细粒度锁来提高并发性能同时避免过度细化导致的死锁风险。
通过合理的设计和使用标准库提供的工具开发者可以有效地避免多线程编程中的条件竞争和死锁问题确保程序的正确性和稳定性。
3.2.5 避免死锁的进阶指导
死锁的原因与常见场景
死锁通常是由对锁的不当使用造成的。例如两个线程互相调用 join() 可能导致死锁因为每个线程都在等待另一个线程结束。类似地当多个线程持有不同锁并试图获取对方持有的锁时也会发生死锁。
为了避免死锁以下是一些进阶的指导意见
避免嵌套锁
建议1避免嵌套锁
最简单的避免死锁的方法是确保每个线程只持有一个锁。如果需要获取多个锁可以使用 std::lock 来一次性锁定多个互斥量从而避免死锁。
std::mutex m1, m2;
std::lock(m1, m2); // 同时锁定m1和m2避免死锁
std::lock_guardstd::mutex lock1(m1, std::adopt_lock);
std::lock_guardstd::mutex lock2(m2, std::adopt_lock);避免在持有锁时调用外部代码
建议2避免在持有锁时调用外部代码
外部代码的行为是不可预测的可能包含获取其他锁的操作这会导致死锁。尽量减少在持有锁的情况下调用外部代码。
使用固定顺序获取锁
建议3使用固定顺序获取锁
当必须获取多个锁时确保所有线程以相同的顺序获取这些锁。例如在链表中删除节点时确保所有线程按相同顺序锁定节点及其相邻节点。
void delete_node(Node* node) {std::lock_guardstd::mutex lock_prev(node-prev-mutex);std::lock_guardstd::mutex lock_next(node-next-mutex);// 确保固定的锁顺序
}使用层次锁结构
建议4使用层次锁结构
为每个互斥量分配一个层级值并确保在任何时刻只能获取比当前层级更低的锁。这样可以避免循环等待的情况。
class hierarchical_mutex {std::mutex internal_mutex;unsigned long const hierarchy_value;unsigned long previous_hierarchy_value;static thread_local unsigned long this_thread_hierarchy_value;void check_for_hierarchy_violation() {if (this_thread_hierarchy_value hierarchy_value) {throw std::logic_error(mutex hierarchy violated);}}void update_hierarchy_value() {previous_hierarchy_value this_thread_hierarchy_value;this_thread_hierarchy_value hierarchy_value;}public:explicit hierarchical_mutex(unsigned long value): hierarchy_value(value), previous_hierarchy_value(0) {}void lock() {check_for_hierarchy_violation();internal_mutex.lock();update_hierarchy_value();}void unlock() {if (this_thread_hierarchy_value ! hierarchy_value) {throw std::logic_error(mutex hierarchy violated);}this_thread_hierarchy_value previous_hierarchy_value;internal_mutex.unlock();}bool try_lock() {check_for_hierarchy_violation();if (!internal_mutex.try_lock()) {return false;}update_hierarchy_value();return true;}
};thread_local unsigned long hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);示例使用层次锁来避免死锁
以下是使用层次锁的一个示例
hierarchical_mutex high_level_mutex(10000);
hierarchical_mutex low_level_mutex(5000);
hierarchical_mutex other_mutex(6000);int do_low_level_stuff();int low_level_func() {std::lock_guardhierarchical_mutex lk(low_level_mutex);return do_low_level_stuff();
}void high_level_stuff(int some_param);void high_level_func() {std::lock_guardhierarchical_mutex lk(high_level_mutex);high_level_stuff(low_level_func());
}void thread_a() {high_level_func();
}void do_other_stuff();void other_stuff() {high_level_func();do_other_stuff();
}void thread_b() {std::lock_guardhierarchical_mutex lk(other_mutex);other_stuff();
}超越锁的延伸扩展
除了上述方法还需要注意其他同步构造中的潜在死锁问题。例如不要在持有锁的情况下等待另一个线程的完成除非你确定该线程的层级低于当前线程。
使用 std::unique_lock 提供灵活性
std::unique_lock 提供了比 std::lock_guard 更多的灵活性。它可以延迟锁定、手动解锁以及在不同作用域之间转移所有权。
示例使用 std::unique_lock 和 std::defer_lock
class some_big_object;
void swap(some_big_object lhs, some_big_object rhs);class X {
private:some_big_object some_detail;std::mutex m;public:X(some_big_object const sd) : some_detail(sd) {}friend void swap(X lhs, X rhs) {if (lhs rhs)return;std::unique_lockstd::mutex lock_a(lhs.m, std::defer_lock);std::unique_lockstd::mutex lock_b(rhs.m, std::defer_lock);std::lock(lock_a, lock_b); // 同时锁定两个互斥量swap(lhs.some_detail, rhs.some_detail);}
};不同域中互斥量的传递
std::unique_lock 支持移动操作可以在不同的作用域之间传递锁的所有权。例如
std::unique_lockstd::mutex get_lock() {extern std::mutex some_mutex;std::unique_lockstd::mutex lk(some_mutex);prepare_data();return lk; // 返回锁的所有权
}void process_data() {std::unique_lockstd::mutex lk(get_lock());do_something(); // 在保护的数据上执行操作
}总结
避免嵌套锁每个线程只持有一个锁必要时使用 std::lock 一次性锁定多个互斥量。避免在持有锁时调用外部代码外部代码可能导致意外的锁竞争。使用固定顺序获取锁确保所有线程以相同的顺序获取锁。使用层次锁结构通过层级值限制锁的获取顺序避免死锁。使用 std::unique_lock 提供灵活性允许延迟锁定、手动解锁及锁的所有权转移。
通过遵循这些指导意见可以有效避免多线程编程中的死锁问题提高程序的稳定性和可靠性。
3.2.8 锁的粒度
锁的粒度简介
锁的粒度指的是通过一个锁保护的数据量大小。细粒度锁fine-grained lock保护较小的数据量而粗粒度锁coarse-grained lock则保护较大的数据量。选择合适的锁粒度对于提高多线程程序的性能至关重要。
类比超市结账场景
考虑一个超市结账的情景如果一位顾客在结账时突然发现忘拿了某样商品离开去取回该商品会导致其他排队的顾客等待。同样地在多线程环境中如果某个线程长时间持有锁其他需要访问共享资源的线程将被迫等待导致整体性能下降。
细粒度锁 vs 粗粒度锁
细粒度锁每个锁保护的数据量较小允许多个线程并行访问不同的数据部分减少竞争和等待时间。粗粒度锁一个锁保护大量数据可能导致更多的线程竞争同一把锁增加等待时间。
示例优化锁的使用
以下是一个示例展示了如何优化锁的使用以减少持锁时间
void get_and_process_data() {std::unique_lockstd::mutex my_lock(the_mutex);some_class data_to_process get_next_data_chunk();my_lock.unlock(); // 1 解锁互斥量避免在处理数据时持有锁result_type result process(data_to_process);my_lock.lock(); // 2 再次上锁准备写入结果write_result(data_to_process, result);
}在这个例子中my_lock.unlock() 在调用 process() 函数之前解锁互斥量从而允许其他线程在此期间访问共享数据。当需要写入结果时再次锁定互斥量。
控制锁的持有时间
为了最小化锁的持有时间可以采取以下策略
只在必要时持有锁仅在访问或修改共享数据时持有锁尽量减少持有锁的时间。分段操作将复杂的操作分成多个步骤并在每个步骤之间释放锁。
示例细粒度锁的应用
假设有一个简单的数据类型 int其拷贝操作非常廉价。在这种情况下可以通过复制数据来避免长时间持有锁
class Y {
private:int some_detail;mutable std::mutex m;int get_detail() const {std::lock_guardstd::mutex lock_a(m); // 1 保护对some_detail的访问return some_detail;}public:Y(int sd) : some_detail(sd) {}friend bool operator(Y const lhs, Y const rhs) {if (lhs rhs)return true;int const lhs_value lhs.get_detail(); // 2 获取lhs的值int const rhs_value rhs.get_detail(); // 3 获取rhs的值return lhs_value rhs_value; // 4 比较两个值}
};在这个例子中比较操作符首先通过调用 get_detail() 成员函数检索要比较的值步骤 2 和 3并在索引时被锁保护步骤 1。然后比较这两个值步骤 4。这种方法减少了锁的持有时间但需要注意的是由于两次获取值之间可能存在数据变化可能会出现条件竞争的问题。
条件竞争与语义一致性
虽然上述方法减少了锁的持有时间但也引入了条件竞争的风险。例如两个值可能在读取后被修改导致比较的结果不再准确。因此在设计并发程序时必须仔细考虑语义一致性问题。
寻找合适的机制
有时单一的锁机制无法满足所有需求。在这种情况下可以考虑使用更复杂的同步机制如读写锁std::shared_mutex、无锁数据结构或其他高级同步技术。
总结
锁的粒度细粒度锁保护较小的数据量适合高并发场景粗粒度锁保护较大的数据量可能导致较多的竞争。控制锁的持有时间尽可能缩短持有锁的时间只在必要的时候持有锁。分段操作将复杂操作分成多个步骤并在每个步骤之间释放锁。条件竞争注意在减少锁持有时间的同时避免引入条件竞争问题。
通过合理选择锁的粒度和控制锁的持有时间可以显著提高多线程程序的性能和可靠性。
3.3 保护共享数据的方式
在多线程编程中互斥量是保护共享数据的一种通用机制但并非唯一方式。根据具体场景选择合适的同步机制可以显著提高程序的性能和可靠性。
3.3.1 保护共享数据的初始化过程
单线程延迟初始化
假设有一个昂贵的资源需要延迟初始化
std::shared_ptrsome_resource resource_ptr;void foo() {if (!resource_ptr) {resource_ptr.reset(new some_resource); // 1 初始化资源}resource_ptr-do_something();
}这段代码在单线程环境中工作良好但在多线程环境中resource_ptr的初始化部分需要保护以避免竞争条件。
多线程延迟初始化
使用互斥量保护初始化过程
std::shared_ptrsome_resource resource_ptr;
std::mutex resource_mutex;void foo() {std::unique_lockstd::mutex lk(resource_mutex);if (!resource_ptr) {resource_ptr.reset(new some_resource); // 只有初始化过程需要保护}lk.unlock();resource_ptr-do_something();
}虽然这种方法保证了线程安全但会导致不必要的序列化降低并发性能。
双重检查锁模式
双重检查锁模式试图减少锁的竞争
void undefined_behaviour_with_double_checked_locking() {if (!resource_ptr) { // 1 不需要锁的读取std::lock_guardstd::mutex lk(resource_mutex);if (!resource_ptr) { // 2 锁保护的读取resource_ptr.reset(new some_resource); // 3 初始化}}resource_ptr-do_something(); // 4 使用资源
}然而这种方法存在潜在的条件竞争问题可能导致未定义行为。
使用 std::call_once 和 std::once_flag
C 标准库提供了 std::call_once 和 std::once_flag 来处理这种情况
std::shared_ptrsome_resource resource_ptr;
std::once_flag resource_flag;void init_resource() {resource_ptr.reset(new some_resource);
}void foo() {std::call_once(resource_flag, init_resource);resource_ptr-do_something();
}这种方式不仅简化了代码还减少了锁的竞争提高了性能。
静态局部变量的线程安全初始化
C11 标准确保静态局部变量的初始化是线程安全的
class my_class;
my_class get_my_class_instance() {static my_class instance; // 线程安全的初始化过程return instance;
}这种初始化方式在多线程调用时也是安全的无需额外的同步机制。
3.3.2 保护不常更新的数据结构
对于不经常更新的数据结构如 DNS 缓存可以使用读者-作者锁reader-writer lock来优化性能。
使用 std::shared_mutex
C17 提供了 std::shared_mutex允许多个读线程同时访问数据而写线程独占访问。
#include map
#include string
#include mutex
#include shared_mutexclass dns_entry;class dns_cache {std::mapstd::string, dns_entry entries;mutable std::shared_mutex entry_mutex;public:dns_entry find_entry(const std::string domain) const {std::shared_lockstd::shared_mutex lk(entry_mutex); // 1 共享锁auto it entries.find(domain);return (it entries.end()) ? dns_entry() : it-second;}void update_or_add_entry(const std::string domain, const dns_entry dns_details) {std::lock_guardstd::shared_mutex lk(entry_mutex); // 2 独占锁entries[domain] dns_details;}
};在这个例子中find_entry() 使用 std::shared_lock 允许多个读线程并发访问而 update_or_add_entry() 使用 std::lock_guard 提供独占访问。
3.3.3 嵌套锁
当一个线程需要多次获取同一个互斥量时可以使用 std::recursive_mutex它允许多次递归锁定而不导致死锁。
使用 std::recursive_mutex
std::recursive_mutex recursive_mutex;void nested_function() {std::lock_guardstd::recursive_mutex lk(recursive_mutex);// 执行操作
}void outer_function() {std::lock_guardstd::recursive_mutex lk(recursive_mutex);nested_function(); // 可以再次锁定同一个互斥量
}需要注意的是嵌套锁应谨慎使用通常应通过重构代码避免嵌套锁定的需求。
总结
锁的粒度选择合适的锁粒度可以提高并发性能细粒度锁适合高并发场景粗粒度锁适合较少竞争的场景。延迟初始化使用 std::call_once 和 std::once_flag 可以有效地保护共享数据的初始化过程避免不必要的锁竞争。读者-作者锁对于不常更新的数据结构使用 std::shared_mutex 可以提高读操作的并发性能。嵌套锁在需要递归锁定的情况下使用 std::recursive_mutex但应尽量避免嵌套锁定的需求。
通过合理选择和使用同步机制可以有效保护共享数据并提升多线程程序的性能和可靠性。