简体   繁体   中英

C++ Lockless Threading Question - Multiple threads iterating over a contiguous array but never accessing same member data?

In my C++ game engine, I have a job system which utilizes worker threads to do various tasks. Threads are affinitized to each available core. Recently, I have been trying to optimize some of my system pipelines by maximizing CPU utilization. Here is some example pseudo-ish code. It isn't an exact replica but the situation is similar.

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();

I then have 5 jobs that operate on this list at the same time with no Mutexes or Critical Sections . Each job function accesses the array via binary search based on a criteria, such as a guid, or just a linear search. Here is the catch. None of the job functions modify the same member data of the entityState ptrs in the entityStateList . However, they can deference the same entityState ptr due to the binary search vs linear search having collisions. But, I repeat, they never modify the same member data at the same time. No member data ptrs are dereferenced at the same time on each thread.

I have run this simulation with a unit test and encountered no issues. However, I have some programmer friends who say there is a very very small probability this will cause undefined behavior with threads pausing and resuming when dereferencing the same entityStatePtr.

The other point I have heard is that the reason this setup has worked, is that the entityState struct size does not fit in a cache line and ends up dividing the data fetching, which in of itself, acts as data protection itself due to the struct data being separated into different cache lines. To clarify, let's say the top half fits in one cache line and the bottom half in another and the job functions only operate on one data member of the entityState ptr and the majority of the time it happens to be on a different cache line. I do not use any atomic modifiers or operations on the member data because no jobs touch the same member data.

Lastly, I also have some programmer friends who say this is perfectly thread safe.

Nevertheless, I have three different statements and my low level knowledge is lacking enough about multi-threading to ascertain which is claim is correct.

The question is... is it possible for a super low crash that could happen in the wild 1 out of 'x' times? Even 1/1million is not acceptable. Is this a safe, lockless threading mechanism to perform multiple operations on the list in parallel? Try to overlook the triviality of the example data. It is much more complex in my engine example. This code can run on multiple OS, such as PC, Linux, and consoles. It has yet to crash but the exposure and testing is limited. I admit I am not a low level expert, but this is saving precious performance time. So, am I waiting to run into a land mine or is this safe? Compiler is gcc version C++11. Also, please avoid the performance topic of locality unless its related to threading and or thread safety. I know cache misses are bad.

The Question - Is is thread safe or not? If yes or no please explain why in detail if possible. I would like to bolster my low level knowledge.

@walnut already explained in detail that "accessing different elements of an array is guaranteed to not cause data races".

However, you mentioned that you have multiple job functions updating the entityState, and that these functions are ordered by some jobchain object. You did not go into detail about how this jobchain is implemented, but you have to ensure that it establishes a proper happens-before relation between the different job functions, otherwise you do have a data race on the entiyState members .

And I also agree with @rustyx - run your code with ThreadSanitizer. It helps unveil a lot of threading issues, including data races.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM