XAudio2 Tutorial 3

Author: Jay Tennant

A Brief Look at XAudio2: Events and Asynchronous I/O (Part 1/2)

XAudio2 is a sound API available on the Windows Vista/7+ and XBox 360 platforms. This tutorial aims at demonstrating in brevity how to use Event objects in a multithreaded application. Whereas this article will focus on an introduction to Event objects, Part 2 focuses on using them with Asynchronous I/O. This discussion is a prerequisite to the tutorial on streaming audio from disk.

The target audience should be at least intermediate level C++ programming. Moderate familiarity with Win32 programming is required.

In this series, we use the rule: code first, ask questions later. So here is the code:

//by Jay Tennant 3/6/12
//demonstrates using Event objects for multiple threads
//win32developer.com
//this code provided free, as in public domain; score!

#include <windows.h>

//all events
HANDLE g_hMessageEvent;
HANDLE g_hAbortEvent;

//the second thread proc
DWORD WINAPI SecondThreadProc( LPVOID pContext );

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
{
	//create the events, all manually reset
	g_hMessageEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
	g_hAbortEvent = CreateEvent( NULL, TRUE, FALSE, NULL );

	//we don't care about this value here, but we must pass it to the CreateThread function
	DWORD dwThreadId = 0;

	//creates the second thread
	HANDLE hSecondThread = CreateThread( NULL, 0, SecondThreadProc, NULL, 0, &dwThreadId );

	//simple message loop
	while( MessageBox( 0, TEXT("Do you want to see a message from another thread?"), TEXT("Main Thread"), MB_YESNO ) == IDYES )
	{
		//signal the event that the second thread should post the message
		SetEvent( g_hMessageEvent );
	}

	//signal the event that the second thread should end
	SetEvent( g_hAbortEvent );

	//wait for the second thread to finish
	WaitForSingleObject( hSecondThread, INFINITE );

	//release the thread
	CloseHandle( hSecondThread );

	//release the events
	CloseHandle( g_hMessageEvent );
	CloseHandle( g_hAbortEvent );

	return 0;
}

DWORD WINAPI SecondThreadProc( LPVOID pContext )
{
	bool quitting = false;

	HANDLE hEvents[2] = { g_hMessageEvent, g_hAbortEvent };

	//the second thread's message loop
	while( !quitting )
	{
		//wait for any events to be signaled
		switch( WaitForMultipleObjects( 2, hEvents, FALSE, INFINITE ) )
		{
		case 0: //the main thread has requested the second thread to post a message
			MessageBox( NULL, TEXT("Hello World!"), TEXT("Second Thread"), MB_OK );
			ResetEvent( g_hMessageEvent );
			break;
		case 1: //the main thread has requested the second thread quit
			quitting = true;
			break;
		default: //something went terribly wrong... :O
			MessageBox( NULL, TEXT("Error!"), TEXT("Second Thread"), MB_OK | MB_ICONEXCLAMATION );
			quitting = true;
		}
	}

	return 0;
}

The Big Event

Events can be simplified in description to be a boolean state that is globally accessible to all an application's threads. (Technically, events can exist cross-process, but we won't cover that here. More information is available on MSDN.)

Commonly, Events are used in conjunction with a WaitForSingleObject() or WaitForMultipleObjects() call. The operation causes the thread to pause on the call until either the state of the event becomes true (or "signaled") or some specified duration of time has run out. So now to analyze the code:

HANDLE g_hMessageEvent;
HANDLE g_hAbortEvent;

A simple way to pass messages to another thread is using a queue. Another handy way is through Events! There are 2 events, one for signaling the second thread should create a message box, and another for signaling the second thread should quit.

g_hMessageEvent = CreateEvent( NULL, TRUE, FALSE, NULL );
g_hAbortEvent = CreateEvent( NULL, TRUE, FALSE, NULL );

Creates the events. The function CreateEvent is defined as:

HANDLE WINAPI CreateEvent(
  __in_opt  LPSECURITY_ATTRIBUTES lpEventAttributes,
  __in      BOOL bManualReset,
  __in      BOOL bInitialState,
  __in_opt  LPCTSTR lpName
);

Right now, we will only end up using the bManualReset and bInitialState parameters. 'bManualReset' enables the application to manually reset the signal state of the Event. What this means is Events that are "automatically reset" will be reset once they are caught in a WaitForSingleObject or similar wait function. "Manual reset" will be reset when ResetEvent is called on the Event. For 'bInitialState', you have the option to set the initial state to signaled (true) if desired.

HANDLE hSecondThread = CreateThread( NULL, 0, SecondThreadProc, NULL, 0, &dwThreadId );

This function creates the second thread. There's detailed information on MSDN about this function, but we will end up using only the 3rd and 4th parameters (4th parameter being the void* to send to the thread function. Obviously, we didn't use it here. :P ) The last parameter isn't necessary for our purposes.

IMPORTANT! According to the documentation on CreateThread, this function does not properly initialize the C Runtime Library (CRT). In low memory conditions, the CRT may terminate the process. The MSDN docs recommend using _beginthreadex() instead if you're going to use the CRT in that thread.

SetEvent( g_hMessageEvent );

This function call sets the state of the selected event to signaled and then continues the message box loop. The second thread's procedure has been sitting, waiting for an event to become signaled. We'll look at it in a bit.

SetEvent( g_hAbortEvent );

Here, the abort event is set to signaled. Again, the second thread has been waiting for an event.

WaitForSingleObject( hSecondThread, INFINITE );

This function waits until a handle becomes signaled. Now, while we use this with Events, conveniently threads will report a signaled state when they have completed and have returned. This function is waiting indefinitely until the second thread finishes and returns.

//release the thread
CloseHandle( hSecondThread );

//release the events
CloseHandle( g_hMessageEvent );
CloseHandle( g_hAbortEvent );

Both threads and events are referred to by handles, and both must be closed the same way.

DWORD WINAPI SecondThreadProc( LPVOID pContext )
{
	bool quitting = false;

	HANDLE hEvents[2] = { g_hMessageEvent, g_hAbortEvent };

	//the second thread's message loop
	while( !quitting )
	{
		//wait for any events to be signaled
		switch( WaitForMultipleObjects( 2, hEvents, FALSE, INFINITE ) )
		{
		case 0: //the main thread has requested the second thread to post a message
			MessageBox( NULL, TEXT("Hello World!"), TEXT("Second Thread"), MB_OK );
			ResetEvent( g_hMessageEvent );
			break;
		case 1: //the main thread has requested the second thread quit
			quitting = true;
			break;
		default: //something went terribly wrong... :O
			MessageBox( NULL, TEXT("Error!"), TEXT("Second Thread"), MB_OK | MB_ICONEXCLAMATION );
			quitting = true;
		}
	}

	return 0;
}

Those with experience in multithreading have used the thread procedure definition before. It must be of the form:

DWORD WINAPI ThreadProc( LPVOID )

One of the important functions in this procedure is WaitForMultipleObjects. Breifly, here is the definition of WaitForSingleObject and WaitForMultipleObjects:

DWORD WINAPI WaitForSingleObject(
  __in  HANDLE hHandle,
  __in  DWORD dwMilliseconds
);

DWORD WINAPI WaitForMultipleObjects(
  __in  DWORD nCount,
  __in  const HANDLE *lpHandles,
  __in  BOOL bWaitAll,
  __in  DWORD dwMilliseconds
);

Pretty self-explanitory. If you want the function to wait indefinitely until the handle becomes signaled, pass INFINITE as the dwMilliseconds parameter. In the second thread's procedure, WaitForMultipleObjects was called with the bWaitAll parameter to FALSE. Obviously, it'd be a terrible mistake to wait indefinitely until both the message event and abort event became signaled.

The other function of interest is ResetEvent. It will manually reset the state of an event to not be signaled (false). This function is not needed on automatically reset events, which are specified at event creation time. Again, if they were created with automatic reset, then after completing WaitForMultipleObjects, they would be reset. It's wise to NOT use that approach in this particular instance, though we will use them in part 2 with some Asynchronous I/O.

Things to Try

Increase the number of threads to 10, all using the same thread proc. Remember to wait for ALL threads to complete before closing.
Change the main thread's while loop to process a YES/NO/CANCEL message box, much like the previous tutorial's. Let the "Yes" button send the message signal, and the "No" button send the quit signal. Watch what happens. (Don't worry, no memory leaks or crashes.)

Additional Information

MSDN entry on thread synchronization: http://msdn.microsoft.com/en-us/library/ms686353(VS.85).aspx
MSDN entry on Event Objects: http://msdn.microsoft.com/en-us/library/ms682655(VS.85).aspx


Next tutorial

Tutorial 4 - XAudio2: Events and Asynchronous I/O (Part 2/2)