DirectPlay Callback Functions and Multithreading Issues

DirectPlay

 
Microsoft DirectX 9.0 SDK Update (Summer 2003)

DirectPlay Callback Functions and Multithreading Issues


Microsoft® DirectPlay® and DirectPlay Voice both require you to implement and register several callback functions to handle the events fired by DirectPlay. If you choose to use multiple DirectPlay threads in your game, it is possible that your application will receive multiple overlapping callbacks. Alternatively, you can use DirectPlay's DoWork mode to avoid multithreading issues.

DirectPlay DoWork Mode

DirectPlay offers a single-threaded environment so that application developers don't have to worry about data corruption or deadlocking due to synchronization problems. Applications simply set the DirectPlay thread count to zero by calling the IDirectPlay8ThreadPool::SetThreadCount method. Then call IDirectPlay8ThreadPool::DoWork within the game loop and DirectPlay will perform all DirectPlay tasks during the time period specified. For more instructions about creating a single-threaded application, see Tutorial 10: DirectPlay Thread Pool.

DirectPlay Thread Pool

For most large-scale, multiplayer networked applications, you will want to implement multithreading because it enables greater scalability. For this, DirectPlay maintains a thread pool, controlled with an IDirectPlay8ThreadPool object. Your callback is invoked on a thread from this pool. The size of this thread pool is configurable on a per-process basis by using the IDirectPlay8ThreadPool::SetThreadCount method.

To correctly and reliably access data in DirectPlay callbacks, you are required to implement a method of multithreading synchronization. This is known as making your callback re-entrant or threadsafe.

The Microsoft Windows® family of operating systems currently offers three methods of synchronizing data in multithreaded environments:

  • Mutex objects (mutually exclusive synchronization objects).
  • Semaphore objects (flag variables used to indicate to potential users that a shared file or other resource is in use).
  • Critical section objects (also provide mutually exclusive synchronization but used only by the threads of a single process).

The DirectPlay voice samples that ship with the Microsoft DirectX® 9.0 software development kit (SDK) demonstrate synchronization using critical section objects. If you want to implement a mutex or semaphore object, read about these topics in the Microsoft Platform Software Development Kit (SDK) as well as in many reference books. Implementing any of these synchronization methods requires an expert knowledge level in these areas due to the level of complexity and difficulty in debugging should any issues arise.

The DirectPlay threading model is optimized for maximum efficiency and there are no thread context switches during "indication" messages, including receive messages.

See Implementing a Callback Function in DirectPlay and DirectPlay Voice for more information.

DirectPlay Networking Callbacks

DirectPlay networking callback functions are of type PFNDPNMESSAGEHANDLER. Depending on the type of networking session, you register the address of your callback function with IDirectPlay8Peer::Initialize, IDirectPlay8Client::Initialize, or IDirectPlay8Server::Initialize.

Synchronization Issues

You must employ one of the three thread synchronization objects in order to maintain the integrity of your game data during processing in a DirectPlay callback.

To understand how your game data could be corrupted, consider that your callback inserts a packet of game data into a structure. Because the callback is reentrant, another thread can enter the callback before the first callback has completed. It is possible that this second thread could also attempt to access the structure at the same location in memory and change the data. Therefore, the data placed in the structure by the first thread is overwritten by the data placed in the structure by the second thread. Please note that this is an oversimplified example of multithreading and there are many other implications to not properly synchronizing multiple threads.

See Implementing a DirectPlay Networking Callback Using Critical Section Objects for an example of how to synchronize data in a DirectPlay networking session.

Worker Threads

You have the option of creating your own "worker threads". A worker thread is another multithreaded application defined callback that is created to process game data independently of the DirectPlay callbacks. The most common way of accomplishing this is to buffer data received during a DirectPlay networking callback thread. Then, a new thread is created and a message is sent to your worker thread callback to notify it to process the buffered data.

Multithreading Performance Issues and Asynchronous Operations

It is important to carefully consider how much time is spent processing messages in DirectPlay callbacks. If you process a lot of data within the DirectPlay callbacks and you employ a data locking mechanism to synchronize threads, you will run into blocking problems as other threads wait to enter the callback.

If you choose to implement a worker thread and offset the processing of game data to another callback, you run the risk of adding a lot of overhead processing time as the CPU switches context between the threads you create and the threads created by DirectPlay. This should be done only if the game data requires a large amount of processing time, and the data is not critical to the real time operation of the game. For example, it is not recommended to process player location data in a worker thread because this data is critical to positioning players in real time within the game.

You can also return DPNSUCCESS_PENDING from the callback, create a pointer to the data buffer, and make that pointer available the worker thread. When the worker thread is finished processing the game data, it calls the ReturnBuffer method of either IDirectPlay8Peer, IDirectPlay8Client, or IDirectPlay8Server, depending on the topology used.

Holding Locks Across API Calls

In general, you should avoid holding shared resource locks across application programming interface (API) calls. This is because it can be hard to envision all the possible interactions with other threads. In the following code, the sending thread is incorrectly holding the pObj->csSomeLock critical section while calling IDirectPlay8Peer::SendTo synchronously.

typedef struct _MYOBJECT{
	CRITICAL_SECTION    csSomeLock;
	DWORD               dwFlags;
	.
	.
	.
} MYOBJECT, *PMYOBJECT;

IDirectPlay8Peer    *pDP8Peer;
PMYOBJECT           pObj;
.
.
.
EnterCriticalSection(&pObj->csSomeLock);
pDP8Peer->SendTo(DPNID_ALL_PLAYERS_GROUP,
                 &dpnBuffer, 1, 0,
                 NULL, NULL, DPNSEND_SYNC);
LeaveCriticalSection(&pObj->csSomeLock);

The local player will receive a copy of the message with a call to the application's message handler on a different thread because the DPNSEND_NOLOOPBACK flag was not used. If the message handler tried to acquire pObj->csSomeLock in response to this message, it would deadlock, because the sending thread cannot return from IDirectPlay8Peer::SendTo (and thus cannot drop the lock) until the message handler returns, but the message handler can't return until the sending thread drops the lock. Instead, use a flag or indexing system so that you can release the lock while you make the API call.

typedef struct _MYOBJECT{
	CRITICAL_SECTION    csSomeLock;
	DWORD               dwFlags;
	.
	.
	.
} MYOBJECT, *PMYOBJECT;

IDirectPlay8Peer     *pDP8Peer;
PMYOBJECT            pObj;
.
.
.
EnterCriticalSection(&pObj->csSomeLock);
pObj->dwFlags |= FLAGS_SENDING;
LeaveCriticalSection(&pObj->csSomeLock);

pDP8Peer->SendTo(DPNID_ALL_PLAYERS_GROUP,
                 &dpnBuffer, 1, 0,
                 NULL, NULL, DPNSEND_SYNC);
				 
EnterCriticalSection(&pObj->csSomeLock);
pObj->dwFlags &= ~FLAGS_SENDING;
LeaveCriticalSection(&pObj->csSomeLock);

© 2003 Microsoft Corporation. All rights reserved.