-1

The Issue

I am currently working on a game. In this game,"game objects" of a certain type are stored in an std::vector. The Update (gameplay) code for these objects is executed on one thread, while rendering is handled by the main thread.

The vector of these "game objects" is passed by reference to the thread handling their Update code, so that the data is shared between threads. As such, some form of thread safety is required in order to prevent the render code from attempting to access memory that is being modified by the update code (which causes a crash).

Ideally, I would like to use a shared_mutex as part of these "game objects" stored within said vector. However, this won't compile and causes errors. My understanding is that this is because a mutex cannot be copied.

Is there a way to solve this problem that is performant, safe, and "not a nightmare to work with"?

The only viable solution I am seeing, is to prevent each specific type of object from updating and rendering at the same time. However, this would go some way to nullify the benefits of multithreading. I would estimate over 80% of game will be processing one singular type of object.

Basic Psuedocode to provide context below:

main(){

   std::vector<Enemies> enemies;

   std::thread(update_thread, std::ref(enemies), ... , ...);

   while(true){
      render_player();
      render_enemies();
      render_physics();

      flip_to_display()
   }
};
update_thread(std::vector<Enemies> &enemies, ... , ...){
   while(true){
      update_player();
      update_enemies();
      update_physics();
   }
};
render_enemies(){
   for(all enemies)
      enemy.render();
}
update_enemies(){
   for(all enemies)
      enemy.update();
}
class Enemies{
   //Would be nice to have the below, but afaik, not possible
   std::shared_mutex mutex;

   update(){
      std::shared_lock<std::shared_mutex> lock(mutex);

      //Write and Access data from enemy
   }

   render(){
      std::shared_lock<std::shared_mutex> lock_shared(mutex);

      //Only Access some data from enemy and draw it
   }
}

NOTE: I am using visual studio 2015, so I can only use C++14(ish) compatible features.

aljowen
  • 31
  • 6
  • Are you sure you fully grasp the implications of this kind of parallelization? How do you render enemies correctly if anther thread is current updating them? Do you render portions of the previous state and portions of the next state as the same frame? Synchronizing individual objects may not be the best approach. Also beware that "updating" usually involves interactions between objects. Locking individual objects may be problematic. You may need to rely on [`std::lock`](https://en.cppreference.com/w/cpp/thread/lock). – François Andrieux Jan 21 '19 at 18:28
  • 1
    Possible duplicate of https://stackoverflow.com/questions/16035250/stl-containers-and-non-copyable-and-non-movable-objects – François Andrieux Jan 21 '19 at 18:30
  • For the first part of what you said, maybe? I know enough to know that I don't know as much as I perhaps should. This is my first time working with CPU multithreading, I have only done OpenCL in the past. For the second part of your question, more or less, yes. Its a simulation puzzle game, and the simulation is predetermined once it begins to run. So it really doesn't matter what frame stuff is rendered on, providing it looks "smooth enough" to the player. – aljowen Jan 21 '19 at 18:33
  • 1
    This approach can certainly be made to work, but you may be more susceptible to annoying timing bugs. Since you don't seem to be using the shareable read property `std::shared_mutex` you should consider just using a `std::mutex`. `std::shared_mutex` won't make your type any more movable. Since it seems the problem is `Enemies` is not movable see the linked duplicate. You could have a `std::unique_ptr` for example. – François Andrieux Jan 21 '19 at 18:37
  • 1
    Please take notice that I edited my comment with an additional concern before you posted your reply. If each object has it's own lock it can be tricky to correctly lock more than one instance when calculating their interactions. It's very likely you will need to rely on [`std::lock`](https://en.cppreference.com/w/cpp/thread/lock) to avoid deadlocks. – François Andrieux Jan 21 '19 at 18:38
  • 1
    Just to answer your XY question (look up what that term means!): In order to store things that are not copyable in any C++ container, just use a pointer, because copying a pointer doesn't require the pointee to be copyable. – Ulrich Eckhardt Jan 21 '19 at 19:36
  • @Ulrich, but since each "game object" within the std::vector would need its own individual mutex to point to, I'm not sure that changes the situation too much. I guess I could custom roll my own form of dynamic list, to store those mutex variables in. But writing my own implementation of std::map (without copy constructors etc) sounds pretty time intensive, especially to meet the levels of robustness that the STL components provide. – aljowen Jan 21 '19 at 19:48
  • 1
    Well, what *is* the situation? You wanted to give every element a mutex and I told you how to do that. That works, but it's not a good MT design. Whenever you do MT, consider cooperation between threads instead of competing for resources. Make sure the renderer doesn't start rendering stuff that doesn't need rendering, e.g. using a "here, I just updated this object would you render it for me" style of communication. – Ulrich Eckhardt Jan 21 '19 at 20:12

1 Answers1

0

Based on the answers provided in the comments, I think my best option will be to find a more creative solution that sidesteps the issue entirely. In this case I will probably process ahead of time and write the results to a buffer. This way Rendering and Processing should by design avoid any conflicts, hence avoiding the need for lock conditions.

Providing I remember, this will most likely become the accepted solution (since the button can't be pressed for 2 days).

aljowen
  • 31
  • 6