do整数读取需要受到关键部分的保护
do integer reads need to be critical section protected?
我在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。
从技术上讲是的,但在许多平台上不是。首先,让我们假设int
是32位(这很常见,但不是几乎通用的)。
32位int
的两个字(16位部分)可能被分别读取或写入。在某些系统上,如果int
没有正确对齐,它们将被单独读取。
想象一下,在一个系统中,您只能进行32位对齐的32位读写(以及16位对齐的16位读写),而int
跨越了这样的边界。最初int
为零(即0x00000000
)
一个线程将0xBAADF00D
写入int
,另一个线程"同时"读取。
写入线程首先将CCD_ 10写入到CCD_。然后,读取器线程读取整个int
(高和低),得到0xBAAD0000
——这是int
从未被故意放入的状态!
写入线程然后写入低位字CCD_ 15。
如前所述,在某些平台上,所有32位读/写都是原子的,所以这不是一个问题。然而,还有其他的担忧。
大多数锁定/解锁代码都包含编译器的指令,以防止跨锁重新排序。如果不防止重新排序,编译器就可以自由地重新排序,只要它在单线程上下文中表现得"好像"它会以这种方式工作。因此,如果在代码中先读取a
,然后再读取b
,那么编译器可以在读取a
之前读取b
,只要它看不到在该时间间隔内修改b
的线程内机会。
因此,您正在读取的代码可能使用这些锁来确保变量的读取按照代码中写入的顺序进行。
下面的评论中提出了其他问题,但我觉得没有能力解决这些问题:缓存问题和可见性。
从这一点来看,arm的内存模型似乎相当宽松,因此您需要一种内存屏障形式,以确保在一个线程中的写入在另一个线程时可见。因此,在您的平台上,您正在做的事情或使用std::atomic似乎是必要的。除非你考虑到这一点,否则你可能会在不同的线程中看到无序的更新,这会破坏你的示例。
我认为您可以使用C++11来确保整数读取是原子的,例如使用std::atomic<int>
。
C++标准规定,如果一个线程同时写入一个变量,而另一个线程从该变量中读取,或者如果两个线程同时向同一变量写入,则存在数据竞争。它进一步指出,数据竞赛会产生未定义的行为。因此,从形式上讲,您必须同步这些读写操作。
当一个线程读取另一个线程写入的数据时,会出现三个独立的问题。首先是撕裂:如果写入需要一个以上的总线周期,那么线程切换可能发生在操作的中间,而另一个线程可能会看到一个写了一半的值;如果读取需要一个以上的总线周期,那么也会出现类似的问题。其次,还有可见性:每个处理器都有自己最近处理的数据的本地副本,写入一个处理器的缓存并不一定会更新另一个处理器。第三,编译器优化可以对读写进行重新排序,这种方式在单个线程中是可以的,但会破坏多线程代码。线程安全代码必须处理这三个问题。这就是同步原语的工作:互斥、条件变量和原子。
尽管整数读/写操作很可能是原子操作,但如果操作不当,编译器优化和处理器缓存仍然会给您带来问题。
为了解释,编译器通常会假设代码是单线程的,并在此基础上进行许多优化。例如,它可能会更改指令的顺序。或者,如果它看到变量只写而从不读,它可能会完全优化它。
CPU也会缓存这个整数,所以如果一个线程写入它,另一个线程可能要很久以后才能看到它。
你可以做两件事。一是像原始代码一样,在关键部分进行包装另一种是将变量标记为显然这是错误的。volatile
。这将向编译器发出信号,表明该变量将被多个线程访问,并将禁用一系列优化,以及在访问该变量时放置特殊的缓存同步指令(也称为"内存屏障")(或者据我所知)
添加:另外,正如另一个答案所指出的,Windows有Interlocked
API,可以用来避免非volatile
变量的这些问题。
- 使用一个考虑到std::map中键值的滚动或换行的键
- 有充分的理由在h文件中使用include保护而不是cpp文件吗
- 为什么在保护模式下继承升级不起作用
- 如何在c++中只将键插入到bimap的一侧
- 访问被拒绝后,c++中的故障保护代码
- C++:无法访问声明的受保护成员
- 使用2个键的cpp-stl::优先级队列排序不正确
- 有效地使用std::unordered_map来插入或增加键的值
- 为什么您需要C++头文件的包含保护
- lock_guard是否保护返回值
- C++映射有2个键,这样任何1个键都可以用来获取值
- 如何在GTK程序运行时禁用屏幕保护程序/电源管理/屏幕消隐
- 智能指针作为无序映射键,并通过引用进行比较
- 带有数组键C++的二进制映射
- 如何将部分流作为参数传递
- 使用 GLUT 使用键停止动画?
- 无法添加多个键以映射将结构作为键
- 将 std::set 与基于键的比较器一起使用
- 如何使用 std::variant 打印地图键/值?
- 编辑/删除受保护的注册表键值