?/TD>
Microsoft DirectX 9.0

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. DirectPlay is multithreaded and will fire multiple events concurrently. It is possible that your application will receive multiple overlapping callbacks.

DirectPlay maintains a thread pool to service callback indications, and your callback is invoked on a thread from the pool of threads maintained by DirectPlay. The size of this thread pool is configurable on a per process basis in Microsoft Windows?2000 and Windows XP. Also, DirectPlay will use I/O completion ports when running on Windows 2000 and Windows XP. I/O completion ports is an advanced topic beyond the scope of this document, and it is recommended that you look in the Microsoft Developer Network documents or in one of the Microsoft Win32?multithreading references currently available.

In order 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 Windows family of operating systems currently offers three methods of synchronizing data in multithreaded environments:

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, these topics are discussed 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);


© 2002 Microsoft Corporation. All rights reserved.