【问题标题】:Is a comparison an atomic operation?比较是原子操作吗?
【发布时间】:2011-04-24 19:48:21
【问题描述】:

下面的比较是原子动作吗?即,是否可以简化为一条 CPU 指令?

char flag = 2;

for(;;)
{
    if (!flag) // <-- this
        break;

    // sleep
}

这就是我正在做的事情:

int main()
{
    sf::Mutex Mutex;
    char flag = 2;

    coordinatorFunction(flag);

    for(;;)
    {
        if (!flag)
            break;

        // sleep
    }
}

void workerFunction(void* a)
{
    char* p = static_cast<char*>(a);

    // work

    GlobalMutex.Lock();
    --*p;
    GlobalMutex.Unlock();
}

void coordinatorFunction(char& refFlag)
{
    sf::Thread worker1(&workerFunction, &refFlag);
    sf::Thread worker2(&workerFunction, &refFlag);

    worker1.Launch();
    worker2.Launch();
}

【问题讨论】:

  • 我认为在进行比较之前总是必须将其读入寄存器,因为您已经传递了指向它的指针。所以不,它不会是原子的。我是这么想的,我不确定。不过,我相信有一些方法可以进行原子比较和写入(我猜不是在 C++ 语言本身内)..
  • 这并不意味着如果某些东西可以简化为一条 CPU 指令,那么它就是原子的。首先,并非所有架构上的所有 CPU 指令都是原子的,其次,仅仅因为某些东西可以简化为原子指令并不意味着它会是原子的。

标签: c++ multithreading mutex atomic operation


【解决方案1】:

这是错误的做法。

您的主线程正在尽可能快地消耗 CPU 周期,除了等待 flag 达到零之外什么都不做。每次尝试此测试都会失败,除了最后一个。不要以这种方式这样做,而是使用您的线程对象最有可能必须使主线程暂停自身的“连接”工具,直到所有工作线程完成。

这样,并非巧合,您不会关心测试是否是原子的,因为您根本不需要它。

【讨论】:

  • 该死的。我应该想到的。谢谢。
  • 我相信原始代码没有忙于等待 :)
【解决方案2】:

任何 C++ 操作都不能保证是原子操作。

【讨论】:

    【解决方案3】:

    在 C++ 中,没有任何东西可以保证是原子的。

    【讨论】:

      【解决方案4】:

      没有。据我所知,C++ 不保证什么是原子的,什么不是——这是特定于平台的。即使您的平台保证可以原子地进行比较,也不能保证您的 C++ 编译器会选择该指令。

      但是,在实践中,简单值类型(例如 char、int、float 等)的比较可能是原子的。但是,您仍然需要注意可能的指令重新排序,无论是在编译器级别还是处理器级别。在这种情况下,这可能无关紧要,但在一般情况下,它可以而且确实如此。您还需要注意,即使比较是原子的,比较后分支也不是原子的 - 因此,如果您尝试使用标志来规范该访问,则两个线程都可以输入相同的代码块。

      如果您想要适当的保证,Windows 上有各种 Interlocked functions 和 gcc 上的 atomic builtins

      【讨论】:

        【解决方案5】:

        不,C++ 不保证任何操作都是原子的。您问题中的代码很可能会被编译为从内存加载到寄存器中,这本身可能需要多条指令,然后是测试。

        【讨论】:

        • 那么我需要更改代码以使其线程安全,还是已经实现了?
        • @Truncheon 术语“线程安全”毫无意义——线程安全完全依赖于上下文。但一般来说,从多个线程访问的任何资源(变量,无论如何)都必须受到某种锁的保护。留给他们自己的设备,编译器永远不会生成“线程安全”的代码。
        【解决方案6】:

        比较不是原子的,因为它需要几个机器语言指令来完成(从内存加载到寄存器等)另外由于内存模型的灵活性和缓存执行测试的线程可能不会“看到”结果马上换另一个话题。

        如果变量标记为 volatile,您可能会安全地进行 简单 测试,但这将是特定于平台的。正如其他人指出的那样,C++ 本身对此不做任何保证。

        【讨论】:

          【解决方案7】:

          比较涉及读取两个数据以及执行实际比较。 数据可以在读取指令和比较指令之间发生变化,因此它不是原子的。

          但是,由于您要比较相等性,_InterlockedCompareExchange instrinsic(用于 x86 中的 lock cmp xchg 指令)可能会满足您的需求,尽管它会涉及替换数据。

          【讨论】:

          • 但是,在许多平台上,如果比较为零,则有一个专用的操作码来执行该操作。这意味着如果值已经在寄存器中,它确实可以原子地完成。
          • @Kylotan:我认为这并不重要,您仍然需要将其读入寄存器并然后进行比较。问题不在于比较,而在于中间状态。但是,如果您有指向内存的指针(而不是寄存器中的值),那么是的,那是正确的;我已经更新了我的答案。
          • 如果该值已经在寄存器中,但是您如何保证呢?
          • 你不会的。我并不反对这个答案的“不”部分。 :) 我只是说比较并不总是涉及读取 2 条数据。
          • 与任何常数进行比较时,重要的不是数据可能在读取和比较指令之间发生变化,而是数据可能在读取时间和 作用时间之间发生变化比较的结果。假设读取是原子的,因为在有人对其进行操作之前,比较不会产生可观察的效果,因此比较是否是原子的就没有任何意义。当然,如果读取不是原子的,那么比较也不会是。
          【解决方案8】:

          您应该问的问题是“是——原子的”?这就是这里的一切。当 flag 达到 0 时你想做点什么。

          你不关心这种情况:

          1. Main thread reads flag, and it is 1.
          2. Worker changes flag with --
          3. Main thread doesn't see that flag is actually 0.
          

          因为在 1 ns 内,主线程循环并再次尝试。

          您确实关心 -- 不是原子的,两个线程同时更改它会跳过递减:

          1. Thread A reads flag, flag is 2
          2. Thread B reads flag, flag is 2
          3. Thread A decrements its copy of flag, 2, and writes to flag, flag is 1
          4. Thread B decrements its copy of flag, also 2, and writes to flag, flag is 1.
          

          你失去了一个减量。你想使用__sync_fetch_and_sub(&flag, 1) 这将自动减少标志。

          最后,绕着床睡觉并不是最好的办法。您想等待条件,等待signal。当工作线程意识到他们已经将标志递减为 0 时,让工作线程提高条件或信号。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-01-06
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-08-05
            • 1970-01-01
            • 1970-01-01
            • 2016-11-05
            相关资源
            最近更新 更多