【问题标题】:Thread Cooperation on Dual-CPU Machines双CPU机器上的线程协作
【发布时间】:2011-04-03 17:29:00
【问题描述】:

我记得在我上大学的一门课程中,我最喜欢的竞争条件示例之一是一个简单的main() 方法启动了两个线程,其中一个将共享(全局)变量加一,其他递减它。伪代码:

static int i = 10;

main() {
    new Thread(thread_run1).start();
    new Thread(thread_run2).start();
    waitForThreads();
    print("The value of i: " + i);
}

thread_run1 {
    i++;
}

thread_run2 {
    i--;
}

教授接着问i 的价值在百万亿次运行之后是多少。 (本质上,如果它不是 10 的话。)不熟悉多线程系统的学生在 100% 的情况下都会响应,print() 语句总是将 i 报告为 10。

这实际上是不正确的,因为我们的教授证明每个递增/递减语句实际上被编译(到汇编)为 3 个语句:

1: move value of 'i' into register x
2: add 1 to value in register x
3: move value of register x into 'i'

因此,i 的值可能是 9、10 或 11。(我不会详细说明。)

我的问题:

我的理解是(是?)物理寄存器集是特定于处理器的。在使用双 CPU 机器时(注意双核和双 CPU 的区别),每个 CPU 是否都有自己的一组物理寄存器? 我以为答案是肯定的。

在单 CPU(多线程)机器上,上下文切换允许每个线程拥有自己的虚拟寄存器集。由于双 CPU 机器上有两组物理寄存器,这难道不会导致更多潜在的竞争条件,因为您实际上可以让两个线程同时运行,而不是在一个单一的“虚拟”同时运行 - CPU机器? (虚拟同时操作是指寄存器状态在每次上下文切换时保存/恢复。)

更具体地说 - 如果您在一台 8 CPU 机器上运行此程序,每个 CPU 有一个线程,是否消除了竞争条件?如果将此示例扩展为使用 8 个线程,在双 CPU 机器上,每个 CPU 有 4 个内核,竞争条件的可能性会增加还是减少? 操作系统如何防止step 3的汇编指令同时在两个不同的CPU上运行?

【问题讨论】:

    标签: multithreading race-condition context-switch


    【解决方案1】:

    是的,双核 CPU 的引入使大量具有潜在线程竞争的程序迅速失败。单核 CPU 多任务由调度程序快速切换线程之间的线程上下文。这消除了与陈旧 CPU 缓存相关的一类线程错误。

    您给出的示例也可能在单核上失败。当线程调度程序在将变量的值加载到寄存器中以增加它时中断线程。它不会那么频繁地失败,因为调度程序中断线程的可能性并不大。

    有一个操作系统功能可以让这些程序在任何情况下都一瘸一拐地运行,而不是在几分钟内崩溃。称为“处理器关联”,可用作 Windows 上 start.exe 的 AFFINITY 命令行选项,winapi 中的 SetProcessAfinityMask()。查看 Interlocked 类,了解以原子方式递增和递减变量的辅助方法。

    【讨论】:

    • 选择作为多CPU和多核之间差异的答案,以及关于单核失败的说明。甚至没有考虑到这一点,这是一个好点。
    【解决方案2】:

    你仍然有一个竞争条件 - 它根本不会改变它。想象一下两个内核同时执行增量 - 它们都加载相同的值,增量到相同的值,然后存储相同的值......所以两个操作的总增量将是一个而不是两个.

    存在其他可能导致内存模型问题的原因 - 第 1 步可能真正无法检索到 i 的最新值,而第 3 步可能不会立即以其他线程可以看到的方式写入i 的新值。

    基本上,这一切都变得非常棘手 - 这就是为什么在访问共享数据时或者使用同步使用无锁更高级别抽象通常是个好主意由真正了解自己在做什么的专家撰写。

    【讨论】:

    • 当然 - 除了 Java 的 Collections.synchronizedMap/Set/List/Collection 方法等高级结构之外,我还熟悉同步技术(互斥量、信号量、同步块)。 - 我只是想知道一个写得不好的程序在双 CPU 机器和双核机器上的功能是否/如何不同。 - 我想在我的所有漫谈之后,我真的只是在寻找我最后一个问题的答案,我相信你已经回答了。 :)
    【解决方案3】:

    首先,双处理器与双核并没有真正的影响。双核处理器在芯片上仍然有两个完全独立的处理器。它们可能共享一些缓存,并且确实共享到内存/外围设备的公共总线,但处理器本身是完全独立的。 (双线程单代码,如超线程)是第三种变体——但每个虚拟处理器也有一组寄存器。两个处理器共享一组执行资源,但它们保留完全独立的寄存器集。

    其次,真正有趣的只有两种情况:单线程执行,以及其他所有情况。一旦您拥有多个线程(即使所有线程都在单个处理器上运行),您就会遇到与在具有数千个处理器的大型机器上运行相同的潜在问题。现在,当代码在更多处理器上运行(最多与您创建的线程一样多)时,您很可能会更快地看到问题出现,但问题本身没有/没有完全改变。

    从实际的角度来看,拥有更多的内核从测试的角度来看很有用。考虑到典型操作系统上任务切换的粒度,编写可以运行而不在单个处理器上显示问题的代码非常容易,这将在几小时甚至几分钟内崩溃和烧毁当您在两个以上或物理处理器上运行它时。但问题并没有真正改变——当你有更多的处理器时,它更有可能更快地出现。

    最终,竞争条件(或死锁、活锁等)与代码的设计有关,而不是与运行它的硬件有关。硬件可以影响您需要采取哪些步骤来强制执行所涉及的条件,但相关差异与处理器的简单数量几乎没有关系。相反,它们是关于当您不仅拥有一台具有多个处理器的单台机器,而是多台具有完全独立的地址空间的机器时做出的让步,因此您可能必须采取额外的步骤来确保当您将值写入内存时它对其他无法直接看到该内存的机器上的 CPU 可见。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-02-07
      • 2016-05-03
      • 2021-02-05
      • 1970-01-01
      • 1970-01-01
      • 2016-08-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多