🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Deal with multi-threaded communication between the Main thread and the client thread

Started by
1 comment, last by hplus0603 5 years ago

Hello guys! I'm programming with a friend a basic 2d videogame with C++ and SFML.

We're using a very basic main-thread in the server that updates every entity etc., and one thread per client that handles the communication with tcpsockets.

However, when I tried to program a multiplayer game in the past, I encountered a common problem with multithreading: my client thread tried to read a main thread variable (like a coordinate) while it was modifying it/deleting it, resulting in a SIGSEGV. In the past, I've resolved this using a list of game snapshots and atomic variables, every time the main thread updated the game it was pushed in a game_snapshot list, creating a list of "game states", the client had just to take the head of the list, convert and send it, while the main thread deleted all the object behind the 100th node (only if one client-thread was not using it).

Is that method correct? We're not currently right now thinking to change the "communication method" (like changing the main thread and one thread per client for something more efficient like async multiplexing), since this is a very basic project done to learn game dev in c++, but the "snapshot method" seems pretty inefficient to me. 100 instances of the same game seems pretty heavy to me.

P.s. No, mutex is not a solution. If a client lags and blocks the access to the game, the game freezes for every one. Same if the client has, for example, 1000+ clients, with every thread trying to mute the variables of the main thread. We're obviously never getting to have more han 4 clients, but this project has been created to learn the basics.

Thanks you so much! Sorry for any spelling mistake, I'm not really good in English :-(

Advertisement

Using atomic variables is very expensive. You don't want to synchronize more than once per tick per thread if you can avoid it.

Typically, there will be double-buffering. The main thread will do something like:

1) wait for all client threads ready
2) flip readable thread input buffers
3) flip writable thread output buffers
4) tell threads to read previously written data
5) read thread inputs
6) simulate into new write buffers

The client threads will do something like:

1) signal ready
2) wait for "go"
3) read from thread output buffers
4) write to thread input buffers
5) flip readable output buffers
6) flip writable input buffers

Note that each thread, and the main function, have their own state of which buffer is "readable" and "writable." Because the section between 1 and 4 in the main thread is a critical section, it's actually possible to avoid that, and just use shared pointers -- main thread reads and writes from the halves that the client threads don't read/write during this particular tick.

 

And, in case you're not familiar; "flipping" just means swapping between buffers A and B; typically updating a couple of pointers.


/* these declarations make the code compile; you're supposed
 * to provide the implementations as part of your game.
#define MAX_PLAYERS 8
struct Vector3{};
struct Quaternion{};
struct Semaphore{
    void wait(int);
    void set(int);
};

template<typename T, typename Q> void do_the_needful(T const &t, Q const &a, Q &b);
template<typename T, typename Q> void do_client_things(Q const &a, T &b);
 */

// One side will read from readBuf() while the other side writes
// to writeBuf(). When both sides are done, call flip(), and the
// reader can now read new-produced data, and the writer can write
// into the buffer that the reader just finished reading. Repeat
// until game over!
template<typename T> class Flipper {
  public:
    Flipper() : flip_(0) {}
    T const &readBuf() const { return buf_[flip_]; }
    T &writeBuf() { return buf_[1-flip_]; }
    void flip() { flip_ = 1-flip_; }
  private:
    T buf_[2];
    int flip_;
};

struct ClientState {
  Vector3 position_;
  Vector3 velocity_;
  Quaternion orientation_;
  Quaternion spin_;
  int hitpoints_;
  int weapon_;
  int ammo_;
  int grenades_;
};

struct GameState {
  ClientState clients_[MAX_PLAYERS];
};

Flipper<GameState> gameFlipper_;

struct ClientInput {
  float move_;
  float turn_;
  float jump_;
  Quaternion aim_;
  bool fireWeapon_;
  bool throwGrenade_;
  bool cycleWeapon_;
};

struct AllInputs {
  ClientInput inputs_[MAX_PLAYERS];
};

Flipper<AllInputs> allInputs_;

Semaphore ready_;
Semaphore clientsGo_;
int numClients_;

void mainThread() {
  ready_.wait(numClients_);
  gameFlipper_.flip();
  allInputs_.flip();
  clientsGo_.set(numClients_);
  auto const &inputs = allInputs_.readBuf().inputs_;
  auto const &oldState = gameFlipper_.readBuf().clients_;
  auto &outputs = gameFlipper_.writeBuf().clients_;
  do_the_needful(inputs, oldState, outputs);
}

void clientThread(int clid) {
  ready_.set(1);
  clientsGo_.wait(1);
  auto const &readFrom = gameFlipper_.readBuf().clients_[clid];
  auto &writeTo = allInputs_.writeBuf().inputs_[clid];
  do_client_things(readFrom, writeTo);
}

 

enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement