10

我遇到了 C++03 一些采用这种形式的代码:

struct Foo {
    int a;
    int b;
    CRITICAL_SECTION cs;
}

// DoFoo::Foo foo_;

void DoFoo::Foolish()
{
    if( foo_.a == 4 )
    {
        PerformSomeTask();

        EnterCriticalSection(&foo_.cs);
        foo_.b = 7;
        LeaveCriticalSection(&foo_.cs);
    }
}

foo_.a是否需要保护读取?例如:

void DoFoo::Foolish()
{
    EnterCriticalSection(&foo_.cs);
    int a = foo_.a;
    LeaveCriticalSection(&foo_.cs);

    if( a == 4 )
    {
        PerformSomeTask();

        EnterCriticalSection(&foo_.cs);
        foo_.b = 7;
        LeaveCriticalSection(&foo_.cs);
    }
}

如果是这样,为什么?

请假设整数是 32 位对齐的。平台是ARM。

4

5 回答 5

11

技术上是的,但在许多平台上都没有。首先,让我们假设它int是 32 位(这很常见,但几乎不是通用的)。

int可能会分别读取或写入32 位的两个字(16 位部分) 。int在某些系统上,如果没有正确对齐,它们将被单独读取。

想象一个系统,您只能执行 32 位对齐的 32 位读取和写入(以及 16 位对齐的 16 位读取和写入),并且int跨越这样的边界。最初int是零(即0x00000000

一个线程写入0xBAADF00Dint另一个“同时”读取它。

写入线程首先0xBAAD写入int. 阅读器线程然后读取整个int(高和低)获取0xBAAD0000- 这是一个int从未被故意放入的状态!

然后写入器线程写入低位字0xF00D

如前所述,在某些平台上,所有 32 位读/写都是原子的,所以这不是问题。然而,还有其他担忧。

大多数锁定/解锁代码都包含对编译器的指令,以防止跨锁重新排序。如果没有防止重新排序,编译器可以自由地重新排序,只要它在单线程上下文中表现得“好像”,它就会以这种方式工作。因此,如果您在代码中阅读athen b,编译器可以在 readb之前阅读a,只要它没有看到b在该时间间隔内进行修改的线程内机会。

因此,您正在阅读的代码可能正在使用这些锁来确保变量的读取按照代码中写入的顺序发生。

下面的评论中提出了其他问题,但我觉得没有能力解决它们:缓存问题和可见性。

于 2012-11-30T18:19:46.337 回答
3

这个角度来看,arm 似乎具有相当宽松的内存模型,因此您需要一种内存屏障形式来确保一个线程中的写入在您期望它们在另一个线程中时是可见的。所以你在做什么,或者在你的平台上使用 std::atomic 似乎是必要的。除非您考虑到这一点,否则您可能会在不同的线程中看到无序更新,这会破坏您的示例。

于 2012-11-30T18:33:46.850 回答
2

我认为您可以使用 C++11 来确保整数读取是原子的,使用 (例如) std::atomic<int>

于 2012-11-30T18:20:20.440 回答
2

C++ 标准规定,如果一个线程写入一个变量的同时另一个线程从该变量读取,或者如果两个线程同时写入同一个变量,则存在数据竞争。它进一步说数据竞争会产生未定义的行为。因此,正式地,您必须同步这些读取和写入。

当一个线程读取另一个线程写入的数据时,会出现三个不同的问题。首先,存在撕裂:如果写入需要多个总线周期,则可能在操作过程中发生线程切换,而另一个线程可能会看到写入一半的值;如果读取需要多个总线周期,则会出现类似的问题。其次,可见性:每个处理器都有自己最近处理的数据的本地副本,写入一个处理器的缓存并不一定会更新另一个处理器的缓存。第三,有编译器优化,以在单个线程中可以接受的方式重新排序读取和写入,但会破坏多线程代码。线程安全代码必须处理所有这三个问题。这就是同步原语的工作:互斥体、条件变量和原子。

于 2012-11-30T19:45:32.877 回答
0

尽管整数读/写操作确实很可能是原子的,但如果您没有正确执行,编译器优化和处理器缓存仍然会给您带来问题。

解释一下 - 编译器通常会假设代码是单线程的,并进行许多依赖于此的优化。例如,它可能会改变指令的顺序。或者,如果它看到变量只被写入而从不被读取,它可能会完全优化它。

CPU 还将缓存该整数,因此如果一个线程写入它,另一个线程可能要到很久以后才能看到它。

你可以做两件事。一种是像原始代码一样包含在关键部分中。另一种是将变量标记为volatile。这将向编译器发出信号,表明该变量将被多个线程访问,并将禁用一系列优化,以及在访问变量周围放置特殊的缓存同步指令(又称“内存屏障”)(或者我理解)。显然这是错误的。

补充:另外,正如另一个答案所指出的,Windows 具有Interlocked可用于避免非volatile变量的这些问题的 API。

于 2012-11-30T18:27:50.287 回答