代码之家  ›  专栏  ›  技术社区  ›  ta.speot.is

螺纹同步101

  •  18
  • ta.speot.is  · 技术社区  · 16 年前

    以前我写过一些非常简单的多线程代码,我一直都知道在我所做的工作中随时都可能有一个上下文切换,所以我总是通过一个进入关键部分的ccriticalsection类来保护对共享变量的访问。把它毁灭。我知道这是相当有侵略性的,我经常进出关键的部分,有时会非常频繁(例如,在函数开始时,我可以将CcriticalSection放在一个更紧密的代码块中),但我的代码没有崩溃,并且运行得足够快。

    在工作中,我的多线程代码需要更紧凑,只需要在最低级别锁定/同步。

    在工作中,我试图调试一些多线程代码,我遇到了:

    EnterCriticalSection(&m_Crit4);
    m_bSomeVariable = true;
    LeaveCriticalSection(&m_Crit4);
    

    现在, m_bSomeVariable 是一个win32 bool(非易失性),据我所知,它被定义为一个int,在x86上,读写这些值是一条指令,并且由于上下文切换发生在指令边界上,因此不需要将此操作与关键部分同步。

    我在网上做了更多的研究,看看这个操作是否不需要同步,我想出了两个场景:

    1. CPU执行顺序错误,或者第二个线程在另一个内核上运行,更新后的值不会写入RAM中供另一个内核查看;以及
    2. int不是4字节对齐的。

    我相信数字1可以用“volatile”关键字来解决。在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
    
        // ...
    
    };
    

    对我来说,这似乎太过分了。我认为关键部分在一个进程之间同步线程,所以如果你有一个进程,你可以进入它,而在那个进程中没有其他线程可以执行。对于您想要保护的每个变量,不需要一个关键部分,如果您在关键部分中,那么没有其他任何东西可以中断您的工作。

    我认为唯一能改变关键部分外部变量的方法是,如果进程与另一个进程共享一个内存页(你能这样做吗?)另一个过程开始改变这些值。互斥体在这里也会有所帮助,命名的互斥体是跨进程共享的,还是只共享同名进程?

    问题3: 我对关键部分的分析是否正确,是否应该重写此代码以使用互斥体?我看过其他同步对象(信号量和旋转锁),它们在这里更适合吗?

    问题4: 关键部分/互斥体/信号量/旋转锁最适合哪里?也就是说,它们应该应用于哪个同步问题。选择一个而不是另一个会有很大的性能损失吗?

    当我们讨论它时,我读到spinlocks不应该在单核多线程环境中使用,而应该只在多核多线程环境中使用。所以, 问题5: 这是错误的,如果不是,为什么是正确的?

    事先感谢您的回复:)

    6 回复  |  直到 13 年前
        1
  •  7
  •   Suma    13 年前

    问题1:使用“volatile”关键字

    在VS2005和以后,C++编译器使用内存屏障包围这个变量的访问,确保变量在使用之前总是被完全写入/读取到主系统内存。

    确切地。如果您不创建可移植代码,Visual Studio将以这种方式实现它。如果你想便携,你的选择目前是“有限的”。直到C++0X,没有可移植的方式如何指定具有保证读/写顺序的原子操作,并且需要实现每个平台解决方案。也就是说,Boost已经为你做了这件肮脏的工作,你可以利用 its atomic primitives .

    问题2:变量需要4字节/8字节对齐?

    如果你让他们保持一致,你是安全的。如果不这样做,规则就很复杂(缓存行,…),因此最安全的方法是保持规则对齐,因为这很容易实现。

    问题3:是否应该重写此代码以使用互斥体?

    关键部分是一个轻量级互斥体。除非需要在进程之间进行同步,否则请使用关键部分。

    问题4:关键部分/互斥锁/信号量/旋转锁在哪里最适合?

    Critical sections 甚至可以 do spin waits 为你。

    问题5:旋转锁不应在单芯中使用

    自旋锁使用这样一个事实:当等待的CPU旋转时,另一个CPU可能会释放该锁。这不能只在一个CPU上发生,因此这只是浪费时间。在多CPU上自旋锁是个好主意,但这取决于自旋等待成功的频率。这个想法是等待一小段时间要比在那里来回切换上下文快得多,因此如果等待时间可能很短,那么最好等待。

        2
  •  13
  •   Goz    16 年前

    1)no volatile只是说每次仍有可能半更新时从内存重新加载值。

    编辑: 2)Windows提供一些原子功能。查找 "Interlocked" functions .

    这些评论让我做了更多的阅读。如果你通读 Intel System Programming Guide 您可以看到对齐的读和写是原子的。

    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处理器。英特尔酷睿2双核,英特尔原子,英特尔酷睿双核,奔腾M, Pentium 4、Intel Xeon和P6系列处理器提供总线控制信号, 允许外部内存子系统进行原子级的拆分访问;但是, 不对齐的数据访问将严重影响处理器的性能,并且 应该避免。 访问大于四字数据的X87指令或SSE指令。 可以使用多个内存访问来实现。如果这样的指令存储 对于内存,一些访问可能完成(写入内存),而另一个访问可能完成 由于体系结构原因(例如由于页表条目)导致操作出错 标记为不存在)。在这种情况下,已完成访问的影响 可能对软件可见,即使整个指令导致了故障。如果TLB 失效被延迟(见第4.10.3.4节),可能会出现此类页面错误。 即使所有访问都是同一页。

    所以基本上是的,如果您从任何地址进行8位读/写,从16位对齐地址等进行16位读/写,您将得到原子操作。值得注意的是,您可以在现代计算机的缓存线内执行未对齐的内存读/写操作。规则看起来很复杂,所以如果我是你的话,我就不会依赖它们了。为评论家干杯,这对我来说是一次很好的学习经历。

    3)关键部分将尝试为其锁旋转锁几次,然后锁定互斥锁。旋转锁定可以吸走CPU的能量而不做任何事情,而互斥体则需要一段时间来完成它的工作。如果不能使用互锁函数,那么关键部分是一个不错的选择。

    4)选择一个选项而不是另一个选项会受到性能惩罚。这是一个非常大的要求,通过这里的一切好处。“msdn帮助”中有很多关于这些的好信息。我建议你读一下。

    5)您可以在单线程环境中使用自旋锁,这通常不是必需的,尽管作为线程管理意味着您不能让两个处理器同时访问相同的数据。这是不可能的。

        3
  •  8
  •   Stack Overflow is garbage    16 年前

    1:挥发性 本身 对于多线程来说几乎是无用的。它保证将执行读/写操作,而不是将值存储在寄存器中,并且保证读/写操作不会被重新排序。 关于其他 volatile 读/写 . 但对于非易失性代码,它仍然可能被重新排序,基本上是99.9%的代码。微软重新定义了 不稳定的 把所有的访问都包装在内存屏障中,但一般情况下并不能保证是这样。它将在任何定义 不稳定的 就像标准一样。(代码将编译并运行,只是不再是线程安全的)

    除此之外,对整数大小的对象的读/写在x86上是原子的,只要对象对齐良好。(你不能保证 什么时候? 不过,写入将发生。编译器和CPU可能会对其重新排序,因此它是原子的,但不是线程安全的)

    2:是的,对象必须对齐才能使读/写成为原子的。

    3:不是真的。只有一个线程可以执行代码 在给定的关键部分内 一次。其他线程仍然可以执行其他代码。所以可以有四个变量,每个变量都受不同的关键部分保护。如果它们都共享同一个关键部分,那么当您操作对象2时,我将无法操作对象1,这不仅效率低下,而且限制了并行性。如果它们受到不同关键部分的保护,我们就不能同时操纵 相同的 对象同时。

    4:旋转锁 很少地 一个好主意。如果您希望线程在获得锁之前只需要等待很短的时间,那么它们非常有用, 你绝对需要最小的延迟。它避免了操作系统上下文切换,这是一个相对缓慢的操作。相反,线程只是位于一个循环中,不断地轮询一个变量。所以CPU使用率更高(在等待spinlock时,核心不会被释放以运行另一个线程),但线程将能够继续 尽快 当锁被释放时。

    至于其他的,性能特征几乎是相同的:只使用最适合您需要的语义。通常,关键部分对于保护共享变量最为方便,互斥体可以很容易地用于设置“标志”,允许其他线程继续进行。

    至于不在单核环境中使用spinlocks,请记住spinlock实际上不会产生。等待spinlock的线程A实际上并没有被挂起,这样操作系统就可以调度线程B运行。但是由于a正在等待这个spinlock,其他线程将不得不释放这个锁。如果您只有一个内核,那么另一个线程只能在A被关闭时运行。有了健全的操作系统,这迟早会发生,作为常规上下文切换的一部分。但既然我们知道A在B有时间执行之前无法获得锁,而且 释放 如果A立即生成,操作系统将其放入等待队列中,然后在B释放锁后重新启动,我们的情况会更好。这就是一切 其他 锁类型有。 旋转锁仍会 工作 在单核环境中(假设操作系统具有抢先的多任务处理),它的效率非常低。

        4
  •  5
  •   Marcelo Cantos    16 年前

    不要使用挥发性物质。它实际上与线程安全无关。见 here 低下身。

    对bool的分配不需要任何同步原语。如果你不做任何特别的努力,它会很好地工作。

    如果要设置变量,然后确保另一个线程看到新值,则需要在两个线程之间建立某种通信。在分配前立即锁定什么也没有,因为在获得锁之前,另一个线程可能已经来了又走了。

    最后一句警告:线程是非常难得到正确的。最有经验的程序员往往对线程的使用最不满意,这应该会为那些不熟悉线程使用的人敲响警钟。我强烈建议您在应用程序中使用一些更高级的原语来实现并发性。通过同步队列传递不可变的数据结构是一种大大减少危险的方法。

        5
  •  3
  •   David Rodríguez - dribeas    16 年前

    易变并不意味着记忆障碍。

    它只意味着它将成为记忆模型感知状态的一部分。这意味着编译器不能将变量优化掉,也不能只在CPU寄存器中对变量执行操作(它将实际加载并存储到内存中)。

    由于没有隐含的内存屏障,编译器可以随意重新排序指令。唯一的保证是不同的可变变量的读/写顺序将与代码中的顺序相同:

    void test() 
    {
        volatile int a;
        volatile int b;
        int c;
    
        c = 1;
        a = 5;
        b = 3;
    }
    

    上面的代码(假设 c 未优化)更新到 C 可能在更新之前或之后发生 a b 提供3种可能的结果。这个 保证按顺序进行更新。 C 任何编译器都可以轻松地进行优化。有了足够的信息,编译器甚至可以进行优化 (如果可以证明没有其他线程读取变量,并且它们没有绑定到硬件数组(因此在本例中,它们实际上可以被删除)。请注意,标准不需要特定的行为,而是一个可感知的状态 as-if 规则。

        6
  •  2
  •   Andy Mortimer    16 年前

    问题3:关键部分和互斥体的工作方式基本相同。win32 mutex是一个内核对象,因此它可以在进程之间共享,并与waitformultipleobjects一起等待,而这是关键的\部分无法做到的。另一方面,关键部分的重量更轻,因此速度更快。但是代码的逻辑不应该受到您所使用的代码的影响。

    您还评论说,“对于您想要保护的每个变量,不需要一个关键部分,如果您在关键部分,那么没有其他任何东西可以打断您。”这是正确的,但是权衡是,访问任何变量都需要您持有该锁。如果变量可以有意义地独立更新,那么您就失去了并行这些操作的机会。(不过,由于这些是同一对象的成员,所以在得出结论之前,我会仔细考虑,它们确实可以相互独立地访问。)