整数读取需要保护关键部分吗?

时间:2012-11-30 18:16:02

标签: c++ winapi concurrency critical-section

我遇到了一些采用这种形式的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的读取是否需要保护? e.g:

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。

5 个答案:

答案 0 :(得分:11)

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

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

想象一个系统,你只能进行32位对齐的32位读写(和16位对齐的16位读写),以及跨越这种边界的int。最初int为零(即0x00000000

一个线程将0xBAADF00D写入int,另一个线程同时读取它“。

写作线程首先将0xBAAD写入int的高位字。读者线程然后读取整个int(高和低)获取0xBAAD0000 - 这是int从未被故意投入的状态!

编写器线程然后写低字0xF00D

如上所述,在某些平台上,所有32位读/写都是原子的,因此这不是一个问题。然而,还有其他问题。

大多数锁定/解锁代码包含编译器的指令,以防止跨锁重新排序。如果没有这种重新排序的预防,编译器可以自由地重新排序,只要它在单个线程上下文中“as-if”行为就可以这样工作。因此,如果您在代码中阅读a然后b,则编译器可以在读取b之前阅读a,只要它没有看到线程内的机会。在该时间间隔内修改b

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

下面的评论中提出了其他问题,但我觉得无法解决这些问题:缓存问题和可见性。

答案 1 :(得分:3)

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

答案 2 :(得分:2)

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

答案 3 :(得分:2)

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

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

答案 4 :(得分:0)

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

要解释 - 编译器通常会假定代码是单线程的,并进行许多依赖于此的优化。例如,它可能会更改指令的顺序。或者,如果它发现变量只是被写入并且从未被读过,那么它可能会完全优化它。

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

你可以做两件事。一种是在原始代码中包含在关键部分。 另一种是将变量标记为volatile。这将告诉编译器这个变量将被多个线程访问,并将禁用一系列优化,以及围绕对变量的访问放置特殊的缓存同步指令(也就是“内存障碍”)(或者我理解)。 显然这是错误的。

已添加另外,正如另一个答案所述,Windows有Interlocked个API,可用于避免非volatile变量出现这些问题。