首页 > 解决方案 > C++ 无锁线程问题 - 多个线程迭代连续数组但从不访问相同的成员数据?

问题描述

在我的 C++ 游戏引擎中,我有一个利用工作线程来执行各种任务的作业系统。线程关联到每个可用的核心。最近,我一直在尝试通过最大化 CPU 利用率来优化我的一些系统管道。这是一些示例伪代码。它不是一个精确的复制品,但情况相似。

struct entityState {
  uint8 * byteBuffer; // Serialized binary data for the Entity
  uint8 * compressedData; // Compressed version of Entity data
  uint64  guid; // Unique ID
  gameTimeMS lastUpdated; // last time buffer was updated in milliseconds
  uint32 numUpdates; // Count of the number of updates
  uint32 numTimesAckedOverNetwork; // How many times client acked the data
  const char * typeData; // Type data in place of RTT
  bool markedForDelete; // Whether this object should be deleted next frame
  const char * debugData; // In debug configs, store meta data 
  // More member data but the point is made
};

// For examples sake, I have a contiguous array of entityState data
List< entityState * > entityStateList;
PopulateListWithEntityStateData(); // ~20,000 entityState ptrs on average
SortEntityStateList();
// Fire off 5 jobs each with their own worker thread
StartEntityStateJobs();

然后,我有 5 个作业同时在此列表上运行,没有MutexesCritical Sections。每个工作职能都通过基于标准的二分搜索访问数组,例如 guid,或者只是线性搜索。这是问题所在。没有任何工作职能修改 entityStateList 中entityState ptrs 的相同成员数据。但是,由于二分搜索与线性搜索存在冲突,它们可以遵循相同的 entityState ptr。但是,我再说一遍,他们从不同时修改相同的成员数据。没有成员数据ptrs 在每个线程上同时被取消引用。

我已经用单元测试运行了这个模拟并且没有遇到任何问题。但是,我有一些程序员朋友说,当取消引用同一个 entityStatePtr 时,这将导致线程暂停和恢复的未定义行为的可能性非常小。

我听说的另一点是此设置起作用的原因是 entityState 结构大小不适合缓存行并最终划分数据获取,由于结构本身,它本身充当数据保护数据被分成不同的缓存行。澄清一下,假设上半部分适合一个缓存行,下半部分适合另一个缓存行,并且作业函数仅对 entityState ptr 的一个数据成员进行操作,并且大部分时间它恰好位于不同的缓存行上。我不对成员数据使用任何原子修饰符或操作,因为没有作业涉及相同的成员数据。

最后,我也有一些程序员朋友说这是完全线程安全的。

尽管如此,我有三种不同的陈述,而且我对多线程的低级知识缺乏足够的知识来确定哪个是正确的。

问题是......是否有可能在“x”次中发生一次超低崩溃?即使是 1/100 万也是不可接受的。这是一种安全、无锁的线程机制,可以在列表上并行执行多个操作吗?尝试忽略示例数据的琐碎性。在我的引擎示例中,它要复杂得多。此代码可以在多个操作系统上运行,例如 PC、Linux 和控制台。它还没有崩溃,但曝光和测试是有限的。我承认我不是低级专家,但这节省了宝贵的表演时间。那么,我是在等着撞上地雷还是这样安全?编译器是 gcc 版本 C++11。另外,请避免局部性的性能话题,除非它与线程和/或线程安全有关。我知道缓存未命中很糟糕。

问题- 线程是否安全?如果是或否,请尽可能详细解释原因。我想加强我的低级知识。

标签: c++multithreadingc++11jobslockless

解决方案


@walnut 已经详细解释了“保证访问数组的不同元素不会导致数据竞争”。

但是,您提到您有多个更新 entityState 的作业函数,并且这些函数由某个作业链对象排序。您没有详细介绍此作业链是如何实现的,但您必须确保它在不同的作业功能之间建立适当的先发生关系,否则您确实会在entiyState 成员上发生数据竞争。

我也同意@rustyx - 使用 ThreadSanitizer 运行您的代码。它有助于揭示许多线程问题,包括数据竞争。


推荐阅读