从网上看到了一篇这个文章,收藏一下
导读:本文是关于C++11标准中修复了双重检查锁定模式的消息,同时作者阐述了实现双重检查锁定模式的诸多方法,并逐一进行了分析,作者还提供了一个在早期编译器上实现双重检查锁定模式的库。
双重检查锁定模式(DCLP)在无锁编程(lock-free programming)中经常被讨论,直到2004年,JAVA才提供了可靠的双重检查锁定实现。而在C++11之前,C++没有提供一种该模式的可移植的可靠实现。
随着双重检查锁定模式在各语言实现上存在的缺点暴露,人们开始研究如何安全可靠地实现它。2000年,一个JAVA高性能研究小组发布了一篇声明《双重检查锁定可能导致锁定无效》。2004年,Scott Meyers 和Andrei Alexandrescu联合发表了一篇名为《C++实现双重检查锁定存在严重缺陷》。这两篇论文都是重点阐述了双重检查锁定(DCLP)是什么,以及双重检查锁定的意义,和当前的各语言实现存在诸多不足。
现如今,JAVA为了安全地实现双重检查锁定修改了其内存模型,并引入了关键词volatile。与此同时,C++构建了一个全新的内存模型和原子操作库(atomic),使得不同编译器实现双重检查锁定(DCLP)更为容易。为了在更早期的C\C++编译器中实现DCLP,在C++11引入了一个名为Mintomic的库,在今年早些时候由我发布了。
过去的一段时间,我都着力于C++中实现DCLP的研究。
什么是双重检查锁定?
如果你想在多线程编程中安全使用单件模式(Singleton),最简单的做法是在访问时对其加锁,使用这种方式,假定两个线程同时调用Singleton::getInstance方法,其中之一负责创建单件:
1
2
3
4
5
6
7
|
Singleton* Singleton::getInstance() { Lock lock; // scope-based lock, released automatically when the function returns
if (m_instance == NULL) {
m_instance = new Singleton;
}
return m_instance;
} |
使用这种方式是可行的,但是当单件被创建之后,实际上你已经不需要再对其进行加锁,加锁虽然不一定导致性能低下,但是在重负载情况下,这也可能导致响应缓慢。
使用双重检查锁定模式避免了在单件对象已经创建好之后进行不必要的锁定,然而实现却有点复杂,在Meyers-Alexandrescu的论文中也有过阐述,文中提出了几种存在缺陷的实现方式,并逐一解释了为什么这样实现存在问题。在论文的结尾的第12页,给出了一种可靠的实现方式,实现依赖一种标准中未规范的内存栅栏技术。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Singleton* Singleton::getInstance() { Singleton* tmp = m_instance;
... // insert memory barrier
if (tmp == NULL) {
Lock lock;
tmp = m_instance;
if (tmp == NULL) {
tmp = new Singleton;
... // insert memory barrier
m_instance = tmp;
}
}
return tmp;
} |
这里,我们可以看到:如模式名称一样,代码中实现了双重校验,在m_instance指针为NULL时,我们做了一次锁定,这一过程在最先创建该对象的线程可见。在创建线程内部构造块中,m_instance被再一次检查,以确保该线程仅创建了一份对象副本。
这是双重检查锁定的实现,只不过在被高亮的代码行中还缺乏了内存栅栏技术做保证,在此文写就之际,C/C++各编译器未对该实现进行统一,而在C++11标准中,对这种情况下的实现进行了完善和统一。
在C++11中获取和释放内存栅栏
在C++11中,你可以获取和释放内存栅栏来实现上述功能(如何获取和释放内存栅栏在我上一篇博文中有讲述)。为了使你的代码在C++各种实现中具备更好的可移植性,你应该使用C++11中新增的atomic类型来包装你的m_instance指针,这使得对m_instance的操作是一个原子操作。下面的代码演示了如何使用内存栅栏,请注意代码高亮部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire); // 编注:原作者提示注意的
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release); // 编注:作者提示注意的
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
} |
上述代码在多核系统中仍然工作正常,这是因为内存栅栏技术在创建对象线程和使用对象线程之间建立了一种“同步-与”的关系(synchronizes-with)。Singleton::m_instance扮演了守卫变量的角色,而单件本身则作为负载内容。
而其他存在缺陷的双重检查锁定实现都缺乏该机制的保障:在没有“同步-与”关系保证的情况下,第一个创建线程的写操作,确切地说是在其构造函数中,可以被其他线程感知,即m_instance指针能被其他线程访问!创建单件线程中的锁也不起作用,由于该锁对其他线程不可见,从而导致在某些情况下,创建对象被执行多次。
如果你想了解关于内存栅栏技术是如何可靠实现双重检查锁定的内部原理,在我的前一篇文章中有一些背景信息(previous post),之前的博客也有一些相关内容。
使用Mintomic 内存栅栏
Mintomic是一个很小的c库,提供了C++11 atomic库中的一些功能函数子集,包含获取和释放内存栅栏,同时它能工作在早期的编译器之上。Mintomic依赖于与C++11相似的内存模型——确切地说是不使用Out-of-thin-air存储——这一技术在早期编译器中未进行实现,而这是在没有C++11标准情况下我们能做的最好实现。以我多年C++多线程开发的经验看来,Out-of-thin-air存储并不流行,而且大多数编译器会避免实现它。
下面的代码演示了如何使用Mintomic的获取和释放内存栅栏机制实现双重检查锁定,基本上与上面的例子类似:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
mint_atomicPtr_t Singleton::m_instance = { 0 }; mint_mutex_t Singleton::m_mutex; Singleton* Singleton::getInstance() { Singleton* tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance);
mint_thread_fence_acquire();
if (tmp == NULL) {
mint_mutex_lock(&m_mutex);
tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance);
if (tmp == NULL) {
tmp = new Singleton;
mint_thread_fence_release();
mint_store_ptr_relaxed(&m_instance, tmp);
}
mint_mutex_unlock(&m_mutex);
}
return tmp;
} |
为了实现获取和释放内存栅栏,Mintomic会试图在其支持的编译器平台产生最高效的机器码。例如,下面的汇编代码来自Xbox 360,使用的是PowerPC处理器。在该平台上,内联的lwsync关键字是针对获取和释放内存栅栏的优化指令。
上述采用C++11标准库编译的例子在PowerPC处理器编译应该会产生一样的汇编代码(理想情况下)。不过,我没有能够在PowerPC下编译C++11来验证这一点。
使用C++11低阶指令顺序约束
在C++11中使用内存栅栏锁定技术可以很方便地实现双重检查锁定。同时也保证在现今流行的多核系统中产生优化的机器码(Mintomic也能做到这一点)。不过使用这种方式并不是常用,在C++11中更好的实现方式是使用保证低阶指令执行顺序约束的原子操作。之前的图片中可以看到,一个写-释放操作可以与一个获取-读操作同步:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load(std::memory_order_acquire);
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
m_instance.store(tmp, std::memory_order_release);
}
}
return tmp;
} |
从技术上讲,使用这种形式的无锁同步比独立内存栅栏技术限制更低。上述操作只是为了防止自身操作的内存排序,而内存栅栏技术则阻止了临近操作的内存排序。尽管如此,现今的x86/64,ARMv6 / v7,和PowerPC处理器架构,针对这两种形式产生的机器码应该是一致的。在我之前的博文中,我展示了C++11低阶指令顺序约束在ARM7中使用了dmb指令,这和使用内存栅栏技术产生的汇编代码相一致。
上述两种方式在Itanium平台可能产生不一样的机器码,在Itanium平台上,C++11标准中的load(memory_order_acquire)可以用单CPU指令:ld.acq,而store(tmp, memory_order_release)使用st.rel就可以实现。
在ARMv8处理器架构中,也提供了和Itanium指令等价的ldar 和 stlr 指令,而不同的地方是:这些指令还会导致stlr和后续ldar之间进一级的存储装载指令进行排序。实际上,ARMv8的新指令试图实现C++11标准中的顺序约束原子操作,这会在后面进一步讲述。
使用C++顺序一致的原子操作
C++11标准提供了一个不同的方式来编写无锁程序(可以把双重检查锁定归类为无锁编程的一种,因为不是所有线程都会获取锁)。在所有原子操作库方法中使用可选参数std::memory_order可以使得所有原子变量变为顺序的原子操作(sequentially consistent),方法的默认参数为std::memory_order_seq_cst。使用顺序约束(SC)原子操作库,整个函数执行都将保证顺序执行,并且不会出现数据竞态(data races)。顺序约束(SC)原子操作和JAVA5版本之后出现的volatile变量很相似。
使用SC原子操作实现双重检查锁定的代码如下:和前面的例子一样,高亮的第二行会与第一次创建单件的线程进行同步与操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance() { Singleton* tmp = m_instance.load();
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load();
if (tmp == nullptr) {
tmp = new Singleton;
m_instance.store(tmp);
}
}
return tmp;
} |
顺序约束(SC)原子操作使得开发者更容易预测代码执行结果,不足之处在于使用顺序约束(SC)原子操作类库的代码效率要比之前的例子低一些。例如,在x64位机器上,上述代码使用Clang3.3优化后产生如下汇编代码:
由于使用了顺序约束(SC)原子操作类库,变量m_instance的存储操作使用了xchg指令,在x64处理器上相当于一个内存栅栏操作。该指令在x64位处理器是一个长周期指令,使用轻量级的mov指令也可以完成操作。不过,这影响不大,因为xchg指令只被单件创建过程调用一次。
不过,在PowerPC or ARMv6/v7处理器上编译上述代码,产生的汇编操作要糟糕得多,具体情形可以参见Herb Sutter的演讲(atomic Weapons talk, part 2.00:44:25 – 00:49:16)。
使用C++11数据顺序依赖原理
上面的例子都是使用了创建单件线程和使用单件其他线程之间的同步与关系。守卫的是数据指针单个元素,开销也是创建单件内容本身。这里,我将演示一种使用数据依赖来保护防卫的指针。
在使用数据依赖时候,上述例子中都使用了一个读-获取操作,这也会产生性能消耗,我们可以使用消费指令来进一步优化。消费指令(consume instruction)非常酷,在PowerPc处理器上它使用了lwsync指令,在ARMv7处理器上则编译为dmd指令。今后我会写一些文章来讲述消费指令和数据依赖机制。
使用C++11静态初始化
一些读者可能已经知道C++11中,你可以跳过之前的检查过程而直接得到线程安全的单件。你只需要使用一个静态初始化:
1
2
3
4
|
Singleton& Singleton::getInstance() { static Singleton instance;
return instance;
} |
C++11标准在6.7.4节中规定:
如果指令逻辑进入一个未被初始化的声明变量,所有并发执行应当等待完成该变量完成初始化。
上述操作在编译时由编译器保证。双重检查锁定则可以利用这一点。编译器并不保证会使用双重检查锁定,但是大部分编译器会这样做。gcc4.6使用-std=c++0x编译选项在ARM处理器产生的汇编代码如下:
由于单件使用的是一个固定地址,编译器会使用一个特殊的防卫变量来完成同步。请注意这里,在初始化变量读操作时没有使用dmb指令来获取一个内存栅栏。守卫变量指向了单件,因此编译器可以使用数据依赖原则来避免使用dmb指令的开销。__cxa_guard_release指令扮演了一个写-释放来解除变量守卫。一旦守卫栅栏被设置,这里存在一个指令顺序强制在读-消费操作之前。这里和前面的例子一样,对内存排序的进行适应性的变更。
前面的长篇累牍主要讲述了C++11标准修复了双层检查锁定实现,并且讲述了其他一些相关知识。
就我个人而言,我认为应当在程序初始化时就初始化一个singleton。使用双重检查锁定可以帮你将任意数据类型存储在一个无锁的哈希表中。这会在后续的文章进一步阐述。
相关推荐
同时,调试代码有助于在开发阶段找出并修复问题。 7. **适配性**:考虑到不同设备的硬件差异,源码可能包含针对不同型号手机的适配代码,确保在各种设备上都能稳定运行。 8. **隐私保护**:由于涉及到用户行为数据...
- **修复**:新的Java内存模型解决了双重检查锁定中的内存可见性问题,使得该模式可以在多线程环境下正确工作。 #### 10. 对编写虚拟机的意义 对于那些想要编写Java虚拟机(JVM)的人来说,理解Java内存模型是非常...
内容概要:本文详细介绍了利用组态王进行石灰石煅烧系统的仿真开发,涵盖实时曲线绘制、报警系统配置、报表生成功能等多个方面。文章从实际案例出发,通过具体的代码片段和操作步骤,讲解了如何实现温度PID控制、设备联锁、能源统计等功能。同时,作者分享了许多实践经验,如解决曲线不刷新问题、优化报警逻辑、提高报表生成效率等,帮助读者快速掌握组态王的核心技术和应用场景。 适合人群:对工业自动化感兴趣的初学者以及希望深入了解组态王使用的工程师。 使用场景及目标:适用于需要构建工业控制系统仿真模型的学习者或开发者,旨在通过实例演练提升对组态王的理解和运用能力,最终能够独立完成类似项目的开发。 其他说明:文中提供了丰富的代码示例和技术细节,有助于读者更好地理解和实践。此外,还特别强调了一些常见的错误和注意事项,如控件命名规范、数据源绑定规则等,确保项目顺利实施。
CAD绘制圆形洞室展示图的初步实现.docx
内容概要:本文详细介绍了基于C语言实现的三电平双机离网并联虚拟同步机控制算法。该算法主要用于解决储能逆变器和UPS系统中双机并联运行时遇到的问题,如功率分配不均、环流超标和动态响应不佳。文中展示了具体的下垂控制、环流抑制、动态响应优化以及中点电位平衡等核心技术的实现方式。此外,还讨论了如何通过自适应虚拟阻抗、状态观测器和查表法等手段提高系统的稳定性和效率。最终,该算法在实际项目中表现出色,实现了高效的功率分配和稳定的动态响应。 适合人群:从事电力电子、储能系统和UPS系统开发的技术人员,尤其是对C语言编程和控制算法有一定了解的研发人员。 使用场景及目标:适用于需要高效、稳定双机并联运行的储能逆变器和UPS系统的设计与开发。主要目标是解决双机并联时的功率分配不均、环流超标和动态响应慢等问题,确保系统在各种工况下的稳定性和可靠性。 其他说明:本文不仅提供了详细的代码实现,还分享了许多实用的调试技巧和实践经验,有助于读者更好地理解和应用这些技术。
方波信号发生器电路仿真,基于LM741运算放大器的方波信号发生器电路仿真设计及实现
2025年电子商务四级培训计划.doc
2023年高中信息技术Excel知识点整理.docx
内容概要:本文详细介绍了三菱FX5U PLC在工业自动化领域的以太网通讯实战经验,涵盖多种常见通讯方式的具体实现方法。主要内容包括:两台PLC之间的Socket通讯,通过C#上位机使用SLMP协议读取PLC寄存器,JE-C伺服电机的以太网控制,以及Modbus RTU和TCP的应用。每种通讯方式均提供了具体的代码示例和调试技巧,确保读者能够快速理解和应用。此外,还特别强调了常见的避坑指南,帮助解决实际操作中可能出现的问题。 适合人群:从事工业自动化、PLC编程及相关领域的工程师和技术人员,特别是那些希望深入了解三菱FX5U PLC以太网通讯机制的人群。 使用场景及目标:适用于需要进行PLC间通讯、PLC与上位机通讯、伺服电机控制以及Modbus协议应用的实际工程项目。目标是提高通讯效率,减少调试时间和错误发生率。 其他说明:文中提供的代码均为经过产线验证的真实程序,可以直接应用于实际项目中。同时,作者分享了许多宝贵的实践经验,有助于读者更好地掌握相关技能并避免常见错误。
计算融合图像(IDL 和 Python 代码)全方位性能评估(APA)指标及绘制 APA 图表的 R 代码
2023年软考信息处理技术员考试历年模拟真题三.docx
内容概要:本文详细介绍了社交聊天即时通讯App的开发要点,涵盖Android和iOS双端原生开发、朋友圈功能、三方云通讯以及支付功能对接。重点讨论了消息系统的优化、朋友圈图片九宫格布局、支付接口的安全性和性能优化等方面的内容。通过具体代码示例展示了如何处理消息发送、接收、状态管理、图片加载、支付回调等问题,并分享了一些实用的经验和技巧。 适合人群:具备一定移动开发经验的开发者,尤其是正在从事或计划从事社交类App开发的技术人员。 使用场景及目标:帮助开发者理解和掌握社交App开发的关键技术和最佳实践,确保消息传输稳定、朋友圈功能流畅、支付安全可靠。适用于希望提高社交App用户体验和技术架构稳健性的开发团队。 其他说明:文中提供了大量实战经验和代码片段,强调了跨平台一致性、性能优化和用户体验的重要性。同时提醒开发者关注平台特异性问题,如iOS和Android之间的差异,以及支付接口的安全校验等。
内容概要:本文详细介绍了信捷XC5-48T PLC和TG765 HMI在三号车间设备改造项目中的应用。重点讨论了四轴伺服控制系统的编程架构、状态机设计、HMI界面设计以及调试工具的使用。文章强调了多轴联动时序控制的重要性,展示了如何通过状态机和互锁机制确保系统的稳定性和可靠性。同时,HMI设计采用了分层式布局和防呆设计,提高了操作的安全性和便捷性。此外,文中还提到了一些实用的调试技巧和注释规范,为后续维护提供了便利。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是对PLC编程和HMI设计有一定基础的人群。 使用场景及目标:适用于需要进行多轴伺服控制系统设计和调试的工程项目。目标是帮助工程师理解和掌握信捷XC5-48T PLC和TG765 HMI的应用方法,提高系统的可靠性和易用性。 其他说明:文章不仅提供了具体的编程示例,还分享了许多实践经验,如状态机设计、互锁机制、防呆设计等,有助于读者更好地理解和应用相关技术。
自定义扩展,简化策略类,方便使用
2025年全国计算机软件专业高级程序员级试题.docx
内容概要:本文详细介绍了V公司提供的UDS协议栈源代码及其在汽车电子开发中的应用。该协议栈以其精简高效的代码结构、良好的底层外设驱动集成以及强大的状态机设计而著称。文中展示了典型的服务路由器函数、Flash驱动接口、CAN通信接口等关键部分的代码片段,并讨论了其在实际项目中的表现和优化技巧。此外,还提到了一些潜在的问题和解决方案,如内存管理和多线程处理等。 适合人群:从事汽车电子开发的技术人员,尤其是对UDS协议栈感兴趣的开发者。 使用场景及目标:适用于需要快速集成UDS协议栈并进行定制化开发的项目。主要目标是在保证稳定性和性能的前提下,减少开发时间和复杂度。 其他说明:文中提供了丰富的实例和实践经验,帮助读者更好地理解和应用V公司的UDS协议栈。同时提醒读者注意特定平台下的兼容性和优化问题。
作者以黄河流域78个地级行政单元为研究区,基于各省水资源公报、社会经济等数据,计算得到黄河流域78地市用水效率与调水工程效用空间格局数据集(2013-2020)。该数据集内容包括以下数据:(1)研究区范围数据;(2)历年各地市用水效率数据;(3)历年各地市水资源压力数据;(4)南水北调效用数据。数据集存储为.shp和.xlsx格式。由9个数据文件组成,数据量为4.41 MB(压缩为1个文件,2.80 MB)Ma, L., Wang, Q. Do water transfer projects promote water use efficiency? Case study of South-to North Water Transfer Project in Yellow River Basin of China. Water 2024, 16: 1367. https://doi.org/10.3390/w16101367.
内容概要:本文详细介绍了利用AD7124实现热电偶和Pt100测温的高精度方案,涵盖硬件设计、冷端补偿算法以及完整的源码实现。首先解释了Pt100的工作原理及其三线制和四线制连接方法,提供了具体的C语言代码示例用于计算Pt100电阻并转化为温度。接着讨论了热电偶的不同类型及其处理方法,展示了如何根据热电偶类型和测量电压计算温度。此外,深入探讨了NTC热敏电阻在冷端补偿中的应用,给出了相应的代码实现。最后,分享了整个方案的硬件设计要点,如AD7124的配置、滤波电路的设计等,并强调了源码的可移植性和优化技巧。 适合人群:从事温度测量领域的工程师和技术人员,尤其是对热电偶和Pt100传感器有一定了解的人群。 使用场景及目标:适用于需要高精度温度测量的应用场合,如工业自动化、实验室设备等。目标是帮助读者掌握基于AD7124的热电偶和Pt100测温系统的实现方法,包括硬件搭建、软件编程和冷端补偿等方面的知识。 其他说明:文中提供的源码和硬件设计方案可以直接应用于STM32平台,同时也适合作为其他MCU平台的参考。对于希望深入了解温度测量技术和提高测量精度的技术人员来说,本文是一份非常有价值的参考资料。
内容概要:本文详细介绍了5kW一字型永磁同步电机的设计过程,涵盖电磁场和温度场的优化。首先,通过调整定子辅助槽的参数,如槽宽、槽深和角度偏移,显著降低了齿槽转矩。其次,在温度场方面,采用高效的水冷系统和合理的散热筋设计,确保电机在高温环境下稳定运行。此外,文中还探讨了极弧系数优化、磁钢排列以及高频噪声抑制等问题。通过MotorCAD进行电磁-热耦合仿真,解决了多个设计难题,实现了高性能和可靠性。 适合人群:电机设计工程师、电磁场仿真技术人员、高校相关专业师生。 使用场景及目标:适用于高功率密度电机设计项目,旨在提高电机性能、降低能耗并延长使用寿命。目标是掌握永磁同步电机的关键设计技术和仿真方法。 其他说明:文中提供了大量Python和Matlab代码示例,帮助读者更好地理解和实践电机设计中的关键技术。同时强调了理论与实践经验相结合的重要性,为实际工程应用提供指导。
内容概要:本文详细介绍了基于逆退火算法改进的人工势场模型及其在路径规划领域的应用。首先指出了传统人工势场模型存在的局限性,即容易陷入局部最优解,难以应对动态变化的障碍物。接着阐述了逆退火算法的核心思想,即通过逐步扩大搜索范围,使模型能够在后期更容易摆脱障碍物的影响,从而准确到达目的地。文中提供了具体的Python代码实现,包括改进后的势场计算函数、路径规划主循环以及可视化部分。此外,还讨论了如何灵活调整障碍物参数,使得模型能够适应不同场景的需求。 适合人群:对路径规划算法感兴趣的科研人员、工程师和技术爱好者,尤其是那些希望深入了解人工势场模型及其改进方法的人群。 使用场景及目标:适用于机器人导航、游戏角色路径规划等领域,旨在提高路径规划的灵活性和准确性,解决传统模型中存在的局部最优问题。 其他说明:文章不仅提供了理论解释,还包括完整的可运行代码示例,便于读者理解和实践。同时,强调了逆退火算法在动态环境中表现出的优势,如快速响应障碍物变化的能力。