在我的 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 个作业同时在此列表上运行,没有Mutexes或Critical Sections。每个工作职能都通过基于标准的二分搜索访问数组,例如 guid,或者只是线性搜索。这是问题所在。没有任何工作职能修改 entityStateList 中entityState ptrs 的相同成员数据。但是,由于二分搜索与线性搜索存在冲突,它们可以遵循相同的 entityState ptr。但是,我再说一遍,他们从不同时修改相同的成员数据。没有成员数据ptrs 在每个线程上同时被取消引用。
我已经用单元测试运行了这个模拟并且没有遇到任何问题。但是,我有一些程序员朋友说,当取消引用同一个 entityStatePtr 时,这将导致线程暂停和恢复的未定义行为的可能性非常小。
我听说的另一点是此设置起作用的原因是 entityState 结构大小不适合缓存行并最终划分数据获取,由于结构本身,它本身充当数据保护数据被分成不同的缓存行。澄清一下,假设上半部分适合一个缓存行,下半部分适合另一个缓存行,并且作业函数仅对 entityState ptr 的一个数据成员进行操作,并且大部分时间它恰好位于不同的缓存行上。我不对成员数据使用任何原子修饰符或操作,因为没有作业涉及相同的成员数据。
最后,我也有一些程序员朋友说这是完全线程安全的。
尽管如此,我有三种不同的陈述,而且我对多线程的低级知识缺乏足够的知识来确定哪个是正确的。
问题是......是否有可能在“x”次中发生一次超低崩溃?即使是 1/100 万也是不可接受的。这是一种安全、无锁的线程机制,可以在列表上并行执行多个操作吗?尝试忽略示例数据的琐碎性。在我的引擎示例中,它要复杂得多。此代码可以在多个操作系统上运行,例如 PC、Linux 和控制台。它还没有崩溃,但曝光和测试是有限的。我承认我不是低级专家,但这节省了宝贵的表演时间。那么,我是在等着撞上地雷还是这样安全?编译器是 gcc 版本 C++11。另外,请避免局部性的性能话题,除非它与线程和/或线程安全有关。我知道缓存未命中很糟糕。
问题- 线程是否安全?如果是或否,请尽可能详细解释原因。我想加强我的低级知识。