ta.*_*.is 18 c++ multithreading mutex volatile memory-barriers
以前我写过一些非常简单的多线程代码,而且我一直都知道在任何时候都可以在我正在做的事情中间进行上下文切换,所以我总是通过以下方式保护访问共享变量一个CCriticalSection类,它进入构造的关键部分并使其破坏.我知道这是相当激进的,我进入和离开关键部分非常频繁,有时非常惊人(例如,当我可以将CCriticalSection置于更严格的代码块中时,在函数的开头)但我的代码没有崩溃并且运行得足够快.
在工作中,我的多线程代码需要更紧密,只需要在最低级别锁定/同步.
在工作中我试图调试一些多线程代码,我遇到了这个:
EnterCriticalSection(&m_Crit4);
m_bSomeVariable = true;
LeaveCriticalSection(&m_Crit4);
Run Code Online (Sandbox Code Playgroud)
现在,m_bSomeVariable是Win32 BOOL(非易失性),据我所知,它被定义为一个int,并且在x86上读取和写入这些值是一条指令,并且因为上下文切换发生在指令边界上,所以没有必要用于将此操作与关键部分同步.
我在网上做了一些更多的研究,看看这个操作是否不需要同步,我想出了两个场景:
我相信使用"volatile"关键字可以解决数字1.在VS2005及更高版本中,C++编译器使用内存屏障围绕对此变量的访问,确保在使用变量之前始终将变量完全写入/读取到主系统内存.
2号我无法验证,我不知道为什么字节对齐会产生影响.我不知道x86指令集,但是mov需要给出一个4字节对齐的地址吗?如果不是,您需要使用指令组合吗?这会引入问题.
所以...
问题1:使用"volatile"关键字(隐含使用内存屏障并暗示编译器不优化此代码)可以避免程序员在读取/读取x86/x64变量之间同步4字节/ 8字节的需要写操作?
问题2:是否明确要求变量为4字节/ 8字节对齐?
我做了一些深入研究我们的代码和类中定义的变量:
class CExample
{
private:
CRITICAL_SECTION m_Crit1; // Protects variable a
CRITICAL_SECTION m_Crit2; // Protects variable b
CRITICAL_SECTION m_Crit3; // Protects variable c
CRITICAL_SECTION m_Crit4; // Protects variable d
// ...
};
Run Code Online (Sandbox Code Playgroud)
现在,对我来说这似乎过分了.我认为关键部分在一个进程之间同步线程,所以如果你有一个,你可以输入它,并且该进程中没有其他线程可以执行.对于要保护的每个变量,不需要关键部分,如果您处于关键部分,那么没有其他任何内容可以打断您.
我认为唯一可以从关键部分外部更改变量的是,如果进程与另一个进程共享一个内存页面(你可以这样做吗?),另一个进程开始更改值.互斥体在这里也有帮助,命名互斥体是跨进程共享的,还是只有同名的进程共享?
问题3:我对关键部分的分析是否正确,是否应该重写此代码以使用互斥锁?我看过其他同步对象(信号量和自旋锁),它们更适合这里吗?
问题4:关键部分/互斥体/信号量/自旋锁哪里最适合?也就是说,它们应该应用于哪个同步问题.选择一个而不是另一个会有很大的性能损失吗?
虽然我们正在研究它,但我读到自旋锁不应该用在单核多线程环境中,只能用于多核多线程环境.所以,问题5:这是错的,或者如果不是,为什么是对的?
在此先感谢任何回复:)
Goz*_*Goz 13
1)没有volatile只是说每次重新加载内存中的值,它仍然可以更新一半.
编辑:2)Windows提供了一些原子功能.查找"Interlocked"功能.
这些评论让我做了一些阅读.如果您阅读了英特尔系统编程指南,您可以看到对齐读取和写入是原子的.
8.1.1保证原子操作Intel486处理器(以及更新的处理器)保证以下基本存储器操作始终以原子方式执行:
•读取或写入字节
•读取或写入在16位边界上对齐的字
•读取或者在32位边界上对齐双字
奔腾处理器(以及更新的处理器)保证以下额外的存储器操作将始终以原子方式执行:
•读取或写入在64位边界上对齐的四字
•16-位访问适合32位数据总线
的未缓存存储器位置P6系列处理器(以及更新的处理器)保证以下额外的存储器操作将始终以原子方式执行:
•未对齐的16-,32-和64-对高速缓存行中的高速缓存内存的位
访问对于可在总线宽度,高速缓存行和页面边界中分割的可高速缓存内存的访问不能保证是原子的 Intel Core 2 Duo,Intel Atom,Intel Core Duo,Pentium M,Pentium 4,Intel Xeon,P6系列,Pentium和Intel486处理器.Intel Core 2 Duo,Intel Atom,Intel Core Duo,Pentium M,Pentium 4,Intel Xeon和P6系列处理器提供总线控制信号,允许外部存储器子系统将分离访问原子化; 但是,非对齐数据访问会严重影响处理器的性能,应该避免.可以使用多个存储器访问来实现访问大于四字的数据的x87指令或SSE指令.如果这样的指令存储到存储器,则一些访问可以完成(写入存储器)而另一个访问导致操作因架构原因而出错(例如,由于标记为"不存在"的页表条目).在这种情况下,即使整个指令导致故障,软件也可以看到完成的访问的效果.如果TLB失效被延迟(参见第4.10.3.4节),即使所有访问都在同一页面,也可能发生此类页面错误.
所以基本上是的,如果你从任何地址进行8位读/写操作,从16位读取/写入16位对齐地址等等你正在进行原子操作.值得注意的是,您可以在现代机器的高速缓存行中进行未对齐的内存读/写操作.规则似乎相当复杂,但如果我是你,我不会依赖它们.为评论者喝彩,这对我来说是一个很好的学习经历:)
3)关键部分将尝试旋转锁定几次,然后锁定互斥锁.旋转锁定可以吸取CPU功率,并且互斥锁可能需要一段时间来完成它的工作.如果您不能使用互锁功能,CriticalSections是一个不错的选择.
4)选择一个而不是另一个会有性能损失.这是一个非常大的要求,以了解这里的一切好处.MSDN帮助有很多关于这些的好信息.我sugegst阅读他们.
5)您可以在单线程环境中使用自旋锁,但这通常不是必需的,因为线程管理意味着您不能让2个处理器同时访问相同的数据.这是不可能的.
1:易失性本身对多线程几乎没用.它保证执行读/写操作,而不是将值存储在寄存器中,并保证读/写不会相对于其他volatile读/写重新排序.但它仍然可以针对非易失性的重新排序,基本上是99.9%的代码.微软已经重新定义volatile了将所有访问包装在内存屏障中,但一般情况下并不能保证这种情况.它将默默地打破任何定义volatile为标准的编译器.(代码将编译并运行,它不再是线程安全的)
除此之外,只要对象很好地对齐,对整数大小的对象的读/写在x86上是原子的.(您无法保证何时会发生写入.编译器和CPU可能会对其进行重新排序,因此它是原子的,但不是线程安全的)
2:是的,必须将对象对齐以使读/写为原子.
3:不是.一次只有一个线程可以在给定的临界区内执行代码.其他线程仍然可以执行其他代码.因此,您可以拥有四个变量,每个变量受不同的关键部分保护.如果它们都共享相同的关键部分,那么当你操作对象2时,我将无法操纵对象1,这是低效的并且不必要地约束并行性.如果它们受到不同关键部分的保护,我们就不能同时操纵同一个对象.
4:自旋锁很少是个好主意.如果您希望线程在获得锁定之前只需要等待很短的时间,那么它们非常有用,并且您绝对需要最小的延迟.它避免了OS上下文切换,这是一个相对较慢的操作.相反,线程只是坐在循环中不断轮询变量.所以更高的CPU使用率(在等待自旋锁时,核心没有被释放以运行另一个线程),但是一旦锁被释放,线程就能够继续.
至于其他的,性能特征几乎相同:只使用最符合您需求的语义.通常,关键部分最便于保护共享变量,并且可以很容易地使用互斥锁来设置允许其他线程继续执行的"标志".
至于在单核环境中不使用自旋锁,请记住螺旋锁实际上不会产生.线程A等待自旋锁实际上没有被搁置,允许操作系统安排线程B运行.但由于A正在等待这个自旋锁,一些其他线程将不得不释放该锁.如果您只有一个核心,那么其他线程只能在A被切换时运行.有了理智的操作系统,无论如何,这将成为常规上下文切换的一部分.但是既然我们知道在B有时间执行并释放锁之前A将无法获得锁定,那么如果A刚刚立即产生,我们会更好,因为操作系统将其置于等待队列中, B释放锁定后重新启动.这就是所有其他锁定类型的功能.自旋锁仍然可以在单核环境中工作(假设具有抢占式多任务处理的操作系统),它的效率非常低.
在VS2005及更高版本中,C++编译器使用内存屏障围绕对此变量的访问,确保在使用变量之前始终将变量完全写入/读取到主系统内存.
究竟.如果您不创建可移植代码,Visual Studio将以这种方式实现它.如果您想要便携,您的选择目前是"有限的".在C++ 0x之前,没有可移植的方法如何指定保证读/写顺序的原子操作,您需要实现每个平台的解决方案.也就是说,boost已经为你做了肮脏的工作,你可以使用它的原子原语.
如果你确保它们保持一致,那么你就是安全的.如果不这样做,规则就很复杂(缓存行,...),因此最安全的方法是保持它们对齐,因为这很容易实现.
关键部分是轻量级互斥体.除非您需要在进程之间进行同步,否则请使用关键部分.
旋转锁定使用以下事实:当等待的CPU正在旋转时,另一个CPU可能会释放锁定.仅使用一个CPU就不会发生这种情况,因此只会浪费时间.在多CPU旋转锁定可能是个好主意,但这取决于旋转等待成功的频率.这个想法是等待一段时间比在那里进行上下文切换要快得多,因此如果等待它可能很短,那么最好等待.
不要使用挥发性物质.它几乎与线程安全无关.看到这里的低位.
BOOL的赋值不需要任何同步原语.没有任何特别的努力,它将工作正常.
如果要设置变量,然后确保另一个线程看到新值,则需要在两个线程之间建立某种通信.只是在分配之前立即锁定没有任何结果,因为在获得锁之前,另一个线程可能已经过去了.
最后一个警告:线程很难做到正确.最有经验的程序员往往最不习惯使用线程,这应该为没有经验的人设置警钟.我强烈建议您使用一些更高级的原语来实现应用程序的并发性.通过同步队列传递不可变数据结构是一种可以大大降低危险的方法.
| 归档时间: |
|
| 查看次数: |
2289 次 |
| 最近记录: |