C++并发编程指南04
文章目录
- 共享数据的问题
- 3.1.1 条件竞争
- 双链表的例子
- 条件竞争示例
- 恶性条件竞争的特点
- 3.1.2 避免恶性条件竞争
- 1. 使用互斥量保护共享数据结构
- 2. 无锁编程
- 3. 软件事务内存(STM)
- 总结
- 互斥量与共享数据保护
- 3.2.1 互斥量
- 使用互斥量保护共享数据
- 示例代码:
- C++17的新特性
- 面向对象设计中的互斥量
- 3.2.2 保护共享数据
- 示例代码:
- 解决方案:
- 3.2.3 接口间的条件竞争
- 示例代码:
- 解决方案:
- 总结
- 接口间的条件竞争与解决方案
- 3.2.3 接口间的条件竞争
- 示例:`std::stack` 容器的实现
- 解决方案:重新设计接口
- 示例:线程安全的堆栈类定义
- 3.2.4 死锁:问题描述及解决方案
- 示例:使用 `std::lock` 和 `std::lock_guard`
- 使用 `std::scoped_lock`(C++17)
- 总结
- 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 <algorithm>std::list<int> some_list; // 1: 全局变量
std::mutex some_mutex; // 2: 全局互斥量void add_to_list(int new_value)
{std::lock_guard<std::mutex> guard(some_mutex); // 3: 锁定互斥量some_list.push_back(new_value);
}bool list_contains(int value_to_find)
{std::lock_guard<std::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
来自动管理互斥量的锁定和解锁,确保在函数执行期间互斥量处于锁定状态,防止其他线程访问共享数据。
C++17的新特性
C++17引入了模板类参数推导,简化了std::lock_guard
的使用:
std::lock_guard guard(some_mutex); // 模板参数类型由编译器推导
此外,C++17还引入了std::scoped_lock
,提供了更强大的功能:
std::scoped_lock guard(some_mutex);
为了兼容C++11标准,本文将继续使用带有模板参数类型的std::lock_guard
。
面向对象设计中的互斥量
将互斥量与需要保护的数据放在同一个类中,可以使代码更加清晰,并且方便了解什么时候对互斥量上锁。例如:
class ProtectedData {
private:std::list<int> data;std::mutex mutex;public:void add_to_list(int new_value) {std::lock_guard<std::mutex> guard(mutex);data.push_back(new_value);}bool contains(int value_to_find) {std::lock_guard<std::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:template<typename Function>void process_data(Function func) {std::lock_guard<std::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::list<int> data;std::mutex mutex;public:const std::list<int>& get_data() { // 返回引用,可能导致条件竞争std::lock_guard<std::mutex> guard(mutex);return data;}
};
在这种情况下,虽然get_data
函数内部使用了互斥量保护数据,但返回的引用可以在互斥量保护范围之外被访问,从而导致潜在的条件竞争。
解决方案:
- 避免返回指向受保护数据的指针或引用,除非这些指针或引用本身也在互斥量保护下使用。
- 设计接口时确保所有对受保护数据的访问都在互斥量保护范围内。
总结
- 互斥量的作用:互斥量用于保护共享数据,确保同一时间只有一个线程能够访问和修改数据,从而避免条件竞争。
std::lock_guard
:利用RAII机制自动管理互斥量的锁定和解锁,简化了代码编写。- 面向对象设计中的互斥量:将互斥量与需要保护的数据放在同一个类中,使得代码更加清晰并便于管理。
- 避免返回指针或引用:确保所有对受保护数据的访问都在互斥量保护下进行,避免返回指向受保护数据的指针或引用。
- 接口设计注意事项:确保接口设计合理,避免通过接口泄露受保护数据的指针或引用,防止条件竞争的发生。
通过正确使用互斥量和精心设计接口,开发者可以有效避免多线程编程中的条件竞争问题,确保程序的正确性和稳定性。
接口间的条件竞争与解决方案
3.2.3 接口间的条件竞争
即使使用了互斥量或其他机制保护共享数据,仍然需要确保数据是否真正受到了保护。例如,在双链表的例子中,为了线程安全地删除一个节点,不仅需要保护待删除节点及其前后相邻的节点,还需要保护整个删除操作的过程。最简单的解决方案是使用互斥量来保护整个链表或数据结构。
示例:std::stack
容器的实现
考虑一个类似于 std::stack
的栈类:
template<typename T, typename Container = std::deque<T>>
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); // C++14的新特性
};
尽管每个成员函数都可能在内部使用互斥量保护数据,但接口设计上的问题仍可能导致条件竞争。例如:
empty()
和size()
:虽然这些函数在返回时可能是正确的,但在返回后其他线程可能会修改栈的内容,导致之前的结果变得不可靠。top()
和pop()
:如果两个线程分别调用top()
和pop()
,可能会出现竞态条件,因为在这两个操作之间,另一个线程可能会修改栈的状态。
解决方案:重新设计接口
为了避免上述问题,可以通过重新设计接口来解决条件竞争:
-
选项1:传入引用获取弹出值
std::vector<int> result; some_stack.pop(result);
缺点:
- 需要构造一个目标类型的实例,这可能不现实或资源开销大。
- 不适用于所有类型,特别是那些没有赋值操作的类型。
-
选项2:无异常抛出的拷贝构造函数或移动构造函数
使用无异常抛出的拷贝构造函数或移动构造函数可以避免某些异常问题,但这限制了可使用的类型范围。
-
选项3:返回指向弹出值的指针
返回一个指向弹出元素的指针(如
std::shared_ptr
)可以避免内存分配问题,并且不会抛出异常。std::shared_ptr<T> pop() {std::lock_guard<std::mutex> lock(m);if (data.empty()) throw empty_stack();std::shared_ptr<T> res(std::make_shared<T>(data.top()));data.pop();return res; }
-
选项4:结合选项1和选项3
提供多个接口选项,让用户选择最适合的方案。
示例:线程安全的堆栈类定义
以下是一个线程安全的堆栈类定义示例,结合了选项1和选项3:
#include <exception>
#include <memory>
#include <mutex>
#include <stack>struct empty_stack : std::exception {const char* what() const throw() {return "empty stack!";}
};template<typename T>
class threadsafe_stack {
private:std::stack<T> data;mutable std::mutex m;public:threadsafe_stack() : data(std::stack<T>()) {}threadsafe_stack(const threadsafe_stack& other) {std::lock_guard<std::mutex> lock(other.m);data = other.data; // 在构造函数体中的执行拷贝}threadsafe_stack& operator=(const threadsafe_stack&) = delete;void push(T new_value) {std::lock_guard<std::mutex> lock(m);data.push(new_value);}std::shared_ptr<T> pop() {std::lock_guard<std::mutex> lock(m);if (data.empty()) throw empty_stack();std::shared_ptr<T> res(std::make_shared<T>(data.top()));data.pop();return res;}void pop(T& value) {std::lock_guard<std::mutex> lock(m);if (data.empty()) throw empty_stack();value = data.top();data.pop();}bool empty() const {std::lock_guard<std::mutex> lock(m);return data.empty();}
};
3.2.4 死锁:问题描述及解决方案
死锁是指两个或多个线程互相等待对方释放资源,导致所有线程都无法继续执行的情况。例如:
- 线程A持有互斥量A并请求互斥量B。
- 线程B持有互斥量B并请求互斥量A。
为了避免死锁,可以采取以下措施:
- 保持一致的加锁顺序:确保所有线程以相同的顺序获取互斥量。
- 使用
std::lock
或std::scoped_lock
:C++标准库提供了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_guard<std::mutex> lock_a(lhs.m, std::adopt_lock); // 2std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock); // 3swap(lhs.some_detail, rhs.some_detail);}
};
使用 std::scoped_lock
(C++17)
C++17引入了 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_guard<std::mutex> lock1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
避免在持有锁时调用外部代码
建议2:避免在持有锁时调用外部代码
外部代码的行为是不可预测的,可能包含获取其他锁的操作,这会导致死锁。尽量减少在持有锁的情况下调用外部代码。
使用固定顺序获取锁
建议3:使用固定顺序获取锁
当必须获取多个锁时,确保所有线程以相同的顺序获取这些锁。例如,在链表中删除节点时,确保所有线程按相同顺序锁定节点及其相邻节点。
void delete_node(Node* node) {std::lock_guard<std::mutex> lock_prev(node->prev->mutex);std::lock_guard<std::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_guard<hierarchical_mutex> lk(low_level_mutex);return do_low_level_stuff();
}void high_level_stuff(int some_param);void high_level_func() {std::lock_guard<hierarchical_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_guard<hierarchical_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_lock<std::mutex> lock_a(lhs.m, std::defer_lock);std::unique_lock<std::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_lock<std::mutex> get_lock() {extern std::mutex some_mutex;std::unique_lock<std::mutex> lk(some_mutex);prepare_data();return lk; // 返回锁的所有权
}void process_data() {std::unique_lock<std::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_lock<std::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_guard<std::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_ptr<some_resource> resource_ptr;void foo() {if (!resource_ptr) {resource_ptr.reset(new some_resource); // 1 初始化资源}resource_ptr->do_something();
}
这段代码在单线程环境中工作良好,但在多线程环境中,resource_ptr
的初始化部分需要保护以避免竞争条件。
多线程延迟初始化
使用互斥量保护初始化过程:
std::shared_ptr<some_resource> resource_ptr;
std::mutex resource_mutex;void foo() {std::unique_lock<std::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_guard<std::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_ptr<some_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();
}
这种方式不仅简化了代码,还减少了锁的竞争,提高了性能。
静态局部变量的线程安全初始化
C++11 标准确保静态局部变量的初始化是线程安全的:
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
C++17 提供了 std::shared_mutex
,允许多个读线程同时访问数据,而写线程独占访问。
#include <map>
#include <string>
#include <mutex>
#include <shared_mutex>class dns_entry;class dns_cache {std::map<std::string, dns_entry> entries;mutable std::shared_mutex entry_mutex;public:dns_entry find_entry(const std::string& domain) const {std::shared_lock<std::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_guard<std::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_guard<std::recursive_mutex> lk(recursive_mutex);// 执行操作
}void outer_function() {std::lock_guard<std::recursive_mutex> lk(recursive_mutex);nested_function(); // 可以再次锁定同一个互斥量
}
需要注意的是,嵌套锁应谨慎使用,通常应通过重构代码避免嵌套锁定的需求。
总结
- 锁的粒度:选择合适的锁粒度可以提高并发性能,细粒度锁适合高并发场景,粗粒度锁适合较少竞争的场景。
- 延迟初始化:使用
std::call_once
和std::once_flag
可以有效地保护共享数据的初始化过程,避免不必要的锁竞争。 - 读者-作者锁:对于不常更新的数据结构,使用
std::shared_mutex
可以提高读操作的并发性能。 - 嵌套锁:在需要递归锁定的情况下,使用
std::recursive_mutex
,但应尽量避免嵌套锁定的需求。
通过合理选择和使用同步机制,可以有效保护共享数据并提升多线程程序的性能和可靠性。
相关文章:
C++并发编程指南04
文章目录 共享数据的问题3.1.1 条件竞争双链表的例子条件竞争示例恶性条件竞争的特点 3.1.2 避免恶性条件竞争1. 使用互斥量保护共享数据结构2. 无锁编程3. 软件事务内存(STM) 总结互斥量与共享数据保护3.2.1 互斥量使用互斥量保护共享数据示例代码&…...
Java实现LRU缓存策略实战
实现LRU模型选择LRU缓存回收算法集成Google Guava(LRU缓存策略)插件Google Guava(LRU策略)缓存示例总结LRU(Least Recently Used,最近最少使用)缓存是一种常见的缓存淘汰策略。它的基本思想是优先保留最近被访问过的数据,淘汰最久未被访问的数据。这种策略的目的是为了…...
三个不推荐使用的线程池
线程池的种类 其实看似这么多的线程池,都离不开ThreadPoolExecutor去创建,只不过他们是简化一些参数 newFixedThreadPool 里面全是核心线程 有资源耗尽的风险,任务队列最大长度为Integer.MAX_VALUE,可能会堆积大量的请求ÿ…...
Golang 并发机制-1:Golang并发特性概述
并发是现代软件开发中的一个基本概念,它使程序能够同时执行多个任务,从而提高效率和响应能力。在本文中,我们将探讨并发性在现代软件开发中的重要性,并深入研究Go处理并发任务的独特方法。 并发的重要性 增强性能 并发在提高软…...
Flink中的时间和窗口
在批处理统计中,我们可以等待一批数据都到齐后,统一处理。但是在实时处理统计中,我们是来一条就得处理一条,那么我们怎么统计最近一段时间内的数据呢?引入“窗口”。 所谓的“窗口”,一般就是划定的一段时…...
Alfresco Content Services dockerCompose自动化部署详尽操作
Alfresco Content Services docker社区部署文档 Alfresco Content Services简介 官方说明书 https://support.hyland.com/r/Alfresco/Alfresco-Content-Services-Community-Edition/23.4/Alfresco-Content-Services-Community-Edition/Using/Content/Folder-rules/Defining-…...
吴恩达深度学习——深层神经网络
来自https://www.bilibili.com/video/BV1FT4y1E74V,仅为本人学习所用。 符号约定 对于该深层网络,有四层,包含三个隐藏层和一个输出层。 隐藏层中,第一层有五个单元、第二层有五个单元,第三层有三个单元。标记 l l l…...
【算法设计与分析】实验1:字符串匹配问题的算法设计与求解
目录 一、实验目的 二、实验环境 三、实验内容 四、核心代码 五、记录与处理 六、思考与总结 七、完整报告和成果文件提取链接 一、实验目的 给定一个文本,在该文本中查找并定位任意给定字符串。 1、深刻理解并掌握蛮力法的设计思想; 2、提高应用…...
C语言二级题解:查找字母以及其他字符个数、数字字符串转双精度值、二维数组上下三角区域数据对调
目录 一、程序填空题 --- 查找字母以及其他字符个数 题目 分析 二、程序修改 --- 数字字符串转双精度值 题目 分析 小数位字符串转数字 三、程序设计 --- 二维数组上下三角区域数据对调 题目 分析 前言 本文来讲解: 查找字母以及其他字符个数、数字字符串…...
Git进阶之旅:Git 配置信息 Config
Git 配置级别: 仓库级别:local [ 优先级最高 ]用户级别:global [ 优先级次之 ]系统级别:system [ 优先级最低 ] 配置文件位置: git 仓库级别对应的配置文件是当前仓库下的 .git/configgit 用户级别对应的配置文件时用…...
Qwen2-VL:在任何分辨率下增强视觉语言模型对世界的感知 (大型视觉模型 核心技术 分享)
摘要 我们推出了Qwen2-VL系列,这是对之前Qwen-VL模型的高级升级,重新定义了视觉处理中的常规预设分辨率方法。Qwen2-VL引入了Naive Dynamic Resolution机制,使模型能够动态地将不同分辨率的图像转换为不同的视觉令牌数量。这种方法允许模型生成更高效和准确的视觉表示,紧密…...
【C语言】在Windows上为可执行文件.exe添加自定义图标
本文详细介绍了在 Windows 环境下,如何为使用 GCC 编译器编译的 C程序 添加自定义图标,从而生成带有图标的 .exe 可执行文件。通过本文的指导,读者可以了解到所需的条件以及具体的操作步骤,使生成的程序更具专业性和个性化。 目录 1. 准备条件2. 具体步骤步骤 1: 准备资源文…...
记录 | Docker的windows版安装
目录 前言一、1.1 打开“启用或关闭Windows功能”1.2 安装“WSL”方式1:命令行下载方式2:离线包下载 二、Docker Desktop更新时间 前言 参考文章:Windows Subsystem for Linux——解决WSL更新速度慢的方案 参考视频:一个视频解决D…...
FortiOS 存在身份验证绕过导致命令执行漏洞(CVE-2024-55591)
免责声明: 本文旨在提供有关特定漏洞的深入信息,帮助用户充分了解潜在的安全风险。发布此信息的目的在于提升网络安全意识和推动技术进步,未经授权访问系统、网络或应用程序,可能会导致法律责任或严重后果。因此,作者不对读者基于本文内容所采取的任何行为承担责任。读者在…...
系统思考—心智模式
“我们的大脑对连贯性的渴望远胜于对准确性的追求。”—诺贝尔经济学得主丹尼尔卡尼曼 在面对复杂的决策时,我们往往更倾向于寻找那些能够迅速串联起来的信息,而非深入挖掘每一个细节的真实性。这种倾向在日常生活中或许能帮助我们迅速作出决策…...
【信息系统项目管理师-选择真题】2008上半年综合知识答案和详解
更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 【第1题】【第2题】【第3题】【第4题】【第5题】【第6题】【第7~8题】【第9题】【第10题】【第11题】【第12题】【第13题】【第14题】【第15题】【第16~20题】【第21题】【第22题】【第23题】【第24题】【第25题…...
深入理解三高架构:高可用性、高性能、高扩展性的最佳实践
引言 在现代互联网环境下,随着用户规模和业务需求的快速增长,系统架构的设计变得尤为重要。为了确保系统能够在高负载和复杂场景下稳定运行,"三高架构"(高可用性、高性能、高扩展性)成为技术架构设计中的核…...
从 SAP 功能顾问到解决方案架构师:破茧成蝶之路
目录 行业瞭望:架构师崭露头角 现状剖析:功能顾问的局限与机遇 能力跃迁:转型的核心要素 (一)专业深度的掘进 (二)集成能力的拓展 (三)知识广度的延伸 ࿰…...
《从因果关系的角度学习失真不变表示以用于图像恢复》学习笔记
paper:2303.06859 GitHub:lixinustc/Causal-IR-DIL: Distortion invariant feature learning for image restoration from a causality perspective 2023 CVPR 目录 摘要 1、介绍 1.1 图像修复任务 1.2 失真不变表示学习 1.3 因果效应估计的挑战…...
STM32 PWM驱动直流电机
接线图: 代码配置: 根据驱动舵机的代码来写,与舵机不同的是,这次的引脚接到了PA2上,所以需要改一下引脚以及改为OC3通道。 另外还需在配置两个GPIO引脚,来控制电机的旋转方向,这里连接到了PA4与…...
【Hadoop】Hadoop 概述
Hadoop 概述 Hadoop 是什么Hadoop 发展历史Hadoop 三大发行版本Hadoop 优势(4 高)Hadoop 组成(面试重点)HDFS 架构概述YARN 架构概述MapReduce 架构概述HDFS、YARN、MapReduce 三者关系 大数据技术生态体系 Hadoop 是什么 Hadoop…...
【仪器分析】FACTs-幅度
** 当然,这回是一篇没有插图的文章,但是有足够多的描述可以用来想象。 我拿这个系列当作前传试试水 引言。正弦信号可能会发生怎样的变化? ** 近日学FACTs,险些成为传函丁真, 如果从仪器角度考察正弦信号的测量&…...
deepseek R1的确不错,特别是深度思考模式
deepseek R1的确不错,特别是深度思考模式,每次都能自我反省改进。比如我让 它写文案: 【赛博朋克版程序员新春密码——2025我们来破局】 亲爱的代码骑士们: 当CtrlS的肌肉记忆遇上抢票插件,当Spring Boot的…...
Unity敌人逻辑笔记
写ai逻辑基本上都需要状态机。因为懒得手搓状态机,所以选择直接用动画状态机当逻辑状态机用。 架构设计 因为敌人的根节点已经有一个animator控制动画,只能增加一个子节点AI,给它加一个animator指向逻辑“动画”状态机。还有一个脚本&#…...
C++,STL 简介:历史、组成、优势
文章目录 引言一、STL 的历史STL 的核心组成三、STL 的核心优势四、结语进一步学习资源: 引言 C 是一门强大且灵活的编程语言,但其真正的魅力之一在于其标准库——尤其是标准模板库(Standard Template Library, STL)。STL 提供了…...
【事务管理】
目录 一. 介绍与操作二. Spring事务管理三. 事务四大特性 \quad 一. 介绍与操作 \quad \quad 二. Spring事务管理 \quad 推荐加在经常进行增删改的方法上 \quad 三. 事务四大特性 \quad ctrlaltt...
ERP革新:打破数据壁垒,重塑市场竞争
标题:ERP革新:打破数据壁垒,重塑市场竞争 文章信息摘要: Operator和Computer Use等工具通过模拟用户交互和自动化数据提取,绕过了传统ERP系统的API限制,打破了其数据护城河。这种技术革新降低了企业切换软…...
小阿卡纳牌
小阿卡纳牌 风:热湿 火:热干 水:冷湿 土:冷干 火风:温度相同,但是湿度不同,二人可能会在短期内十分热情,但是等待热情消退之后,会趋于平淡。 湿度相同、温度不同&#x…...
android的gradle
资料: GitHub - ChenSWD/CopyGradleInAction: 备份《Gradle IN Action》书中的源码,添加了部分注释 //github上一个开源项目,外加pdf书 Gradle User Manual gradle官网 讲的挺好的博客 Gradle之重新认识Gradle(项目结构、命令行、tas…...
时间轮:XXL-JOB 高效、精准定时任务调度实现思路分析
大家好,我是此林。 定时任务是我们项目中经常会遇到的一个场景。那么如果让我们手动来实现一个定时任务框架,我们会怎么做呢? 1. 基础实现:简单的线程池时间轮询 最直接的方式是创建一个定时任务线程池,用户每提交一…...
【愚公系列】《循序渐进Vue.js 3.x前端开发实践》029-组件的数据注入
标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主&…...
「 机器人 」扑翼飞行器控制的当前挑战与后续潜在研究方向
前言 在扑翼飞行器设计与控制方面,虽然已经取得了显著的进步,但在飞行时间、环境适应性、能量利用效率及模型精度等方面依旧存在亟待解决的挑战。以下内容概括了这些挑战和可能的改进路径。 1. 当前挑战 1.1 飞行时间短 (1)主要原因 能源存储有限(电池容量小)、驱动系…...
ICSE‘25 LLM Assistance for Memory Safety
不知道从什么时候开始,各大技术社区,技术群聊流行着 “用Rust重写!” ,放一张图(笑死… 这不, 随着大模型技术的流行,大家都在探索如何让大模型自动完成仓库级别(全程序)的代码重构,代码变换(Refactor&…...
使用 Docker 运行 Oracle Database 23ai Free 容器镜像并配置密码与数据持久化
使用 Docker 运行 Oracle Database 23ai Free 容器镜像并配置密码与数据持久化 前言环境准备运行 Oracle Database 23ai Free 容器基本命令参数说明示例 注意事项高级配置参数说明 总结 前言 Oracle Database 23ai Free 是 Oracle 提供的免费版数据库,基于 Oracle …...
在Linux系统上安装.NET
测试系统:openKylin(开放麒麟) 1.确定系统和架构信息: 打开终端(Ctrl Alt T),输入cat /etc/os-release查看系统版本相关信息。 输入uname -m查看系统架构。确保你的系统和架构符合.NET 的要求,如果架构…...
C++ unordered_map和unordered_set的使用,哈希表的实现
文章目录 unordered_map,unorder_set和map ,set的差异哈希表的实现概念直接定址法哈希冲突哈希冲突举个例子 负载因子将关键字转为整数哈希函数除法散列法/除留余数法 哈希冲突的解决方法开放定址法线性探测二次探测 开放定址法代码实现 哈希表的代码 un…...
星火大模型接入及文本生成HTTP流式、非流式接口(JAVA)
文章目录 一、接入星火大模型二、基于JAVA实现HTTP非流式接口1.配置2.接口实现(1)分析接口请求(2)代码实现 3.功能测试(1)测试对话功能(2)测试记住上下文功能 三、基于JAVA实现HTTP流…...
数据结构——二叉树——堆(1)
今天,我们来写一篇关于数据结构的二叉树的知识。 在学习真正的二叉树之前,我们必不可少的先了解一下二叉树的相关概念。 一:树的概念 树是一种非线性的数据结构,它是由n(n>0)个有限结点组成一个具有层…...
【HarmonyOS之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(二)
目录 1 -> HML语法 1.1 -> 页面结构 1.2 -> 数据绑定 1.3 -> 普通事件绑定 1.4 -> 冒泡事件绑定5 1.5 -> 捕获事件绑定5 1.6 -> 列表渲染 1.7 -> 条件渲染 1.8 -> 逻辑控制块 1.9 -> 模板引用 2 -> CSS语法 2.1 -> 尺寸单位 …...
SOME/IP--协议英文原文讲解4
前言 SOME/IP协议越来越多的用于汽车电子行业中,关于协议详细完全的中文资料却没有,所以我将结合工作经验并对照英文原版协议做一系列的文章。基本分三大块: 1. SOME/IP协议讲解 2. SOME/IP-SD协议讲解 3. python/C举例调试讲解 4.1.3 End…...
【AI】【本地部署】OpenWebUI的升级并移植旧有用户信息
【背景】 OpenWebUI的版本升级频率很高,并会修改旧版本的Bug,不过对于已经在使用的系统,升级后现有用户信息都会丢失,于是研究如何在升级后将现有的用户信息移植到升级后版本。 【准备工作】 OpenWebUI的升级步骤在Docker中有现…...
论文笔记(六十三)Understanding Diffusion Models: A Unified Perspective(五)
Understanding Diffusion Models: A Unified Perspective(五) 文章概括基于得分的生成模型(Score-based Generative Models) 文章概括 引用: article{luo2022understanding,title{Understanding diffusion models: A…...
Tableau:为数据科学专家量身定制
Tableau是一款功能强大且广泛应用的数据可视化和商业智能(BI)工具,由斯坦福大学的三位研究者于2003年创立。它旨在通过直观的界面和强大的功能,帮助用户轻松地探索、分析和呈现数据,从而做出更明智的决策。 核心功能与…...
CAG技术:提升LLM响应速度与质量
标题:CAG技术:提升LLM响应速度与质量 文章信息摘要: CAG(Cache-Augmented Generation)通过预加载相关知识到LLM的扩展上下文中,显著减少了检索延迟和错误,从而提升了响应速度和质量。与传统的R…...
飞桨PaddleNLP套件中使用DeepSeek r1大模型
安装飞桨PaddleNLP 首先安装最新的PaddleNLP3.0版本: pip install paddlenlp3.0.0b3 依赖库比较多,可能需要较长时间安装。 安装好后,看看版本: import paddlenlp paddlenlp.__version__ 输出: 3.0.0b3.post2025…...
单片机基础模块学习——NE555芯片
一、NE555电路图 NE555也称555定时器,本文主要利用NE555产生方波发生电路。整个电路相当于频率可调的方波发生器。 通过调整电位器的阻值,方波的频率也随之改变。 RB3在开发板的位置如下图 测量方波信号的引脚为SIGHAL,由上面的电路图可知,NE555已经构成完整的方波发生电…...
php:代码中怎么搭建一个类似linux系统的crontab服务
一、前言 最近使用自己搭建的php框架写一些东西,需要用到异步脚本任务的执行,但是是因为自己搭建的框架没有现成的机制,所以想自己搭建一个类似linux系统的crontab服务的功能。 因为如果直接使用linux crontab的服务配置起来很麻烦࿰…...
Versal - 基础3(AXI NoC 专题+仿真+QoS)
目录 1. 简介 2. 示例 2.1 示例说明 2.2 创建项目 2.2.1 平台信息 2.2.2 AXI NoC Automation 2.2.3 创建时钟和复位 2.3 配置 NoC 2.4 配置 AXI Traffic 2.5 配置 Memory Size 2.6 Validate BD 2.7 添加观察信号 2.8 运行仿真 2.9 查看结果 2.9.1 整体波形 2.9…...
Synology 群辉NAS安装(10)安装confluence
Synology 群辉NAS安装(10)安装confluence 写在前面本着一朝鲜吃遍天的原则,我又去了这个github的作者那里翻车的第一次尝试手工创建数据库制作一个新的docker-compose of confluence 不折腾但成功启动的版本 写在前面 在装完jira之后&#x…...
挂载mount
文章目录 1.挂载的概念(1)挂载命令:mount -t nfs(2)-t 选项:指定要挂载的文件系统类型(3)-o选项 2.挂载的目的和作用(1)跨操作系统访问:将Windows系统内容挂载到Linux系统下(2)访问外部存储设备(3)整合不同的存储设备 3.文件系统挂载要做的事…...