I’m working on adding a ranking server and a lobby server to RakNet. One of the problems this entails is that database calls are blocking. It’s possible that the ranking server or lobby server will be doing other things at the same time (such as running the game), especially for smaller games. This means that database calls have to be run in a thread.

I spent a few hours setting up the ranking server calls to execute in a thread by copying the input parameters, doing the function, copying the output, and calling the result back in the main thread. But this was really a pain in the butt. It’s not the first time I’ve had to have done it. And I’ll have to do it again, but much more so, for the lobby server.

This morning when I woke up I thought of a solution that so far is turning out well. Create one thread that takes a stream of functors, does processing on those functors, and when done calls a function in the main thread via a callback. The functor itself will have as member variables what was formerly the input and output parameter lists. This avoids the necessity of copying parameters. And since it is up to the user to derive the functor, they can add contextual user data or extend it as they wish. (As opposed to passing a void* and worrying about allocation).

Here it is, leaving out the implementation


/// FunctionThread takes a stream of classes that implement a processing function,
/// processes them in a thread, and calls a callback with the result.
/// It's a useful way to call blocking functions that you do not want to block,
/// such as file writes and database operations.
class FunctionThread
{
public:
FunctionThread();
~FunctionThread();

/// Starts the thread up.
void StartThread(void);

/// Stop processing. Will also call FunctorResultHandler callbacks
/// with /a wasCancelled set to true.
/// \param[in] blockOnCurrentProcessing Wait for the current
/// processing to finish?
void StopThread(bool blockOnCurrentProcessing);

/// Add a functor to the incoming stream of functors
void Push(Functor *functor, void *context);

/// Call FunctorResultHandler callbacks
/// Normally you would call this once per update cycle, although you
/// do not have to.
void CallResultHandlers(void);
}

/// A functor is a single unit of processing to send to the Function thread.
/// Derive from it, add your data, and implement the processing function.
class Functor
{
public:
virtual void Process(void)=0;
/// Called from FunctionThread::CallResultHandlers with wasCancelled
/// false OR Called from FunctionThread::StopThread or
/// FunctionThread::~FunctionThread with wasCancelled true
virtual void HandleResult(bool wasCancelled, void *context) {};
};

Here’s one of the smaller functors. It gets a list of players given a game database id.


/// Given a particular game, get ratings for all players
class GetRatingForPlayers_Functor : public Functor
{
public:
/// Identifies in the game in the database with a primary
/// and secondary integer key. User defined.
RankingServerDBInterface::EntityDBID gameDbId;

// OUTPUT
/// List of players and rankings for a given gameId
DataStructures::List < rankingserverdbinterface::RatedEntity > outputList;
};

Before I had to

1. Add member functions for every operation
2. Add a callback for every member function
3. Copy the input parameters for every operation and store them in an input list
4. Copy the result of the operation and store them in an output list.
5. Create a thread to do the functionality
6. Create interfaces for every operation, that actually does the work in the database

Now I can
1. Create a functor for every operation
2. Implement the functor
3. Pass the functor to the FunctionThread instance

Another nice thing about this is I can reuse the same FunctionThread for both the ranking server and the lobby server. The old way would have created a thread for each of those classes.

Average Rating: 4.9 out of 5 based on 272 user reviews.

Leave a Reply