Wednesday, November 15, 2006

Cross thread calls in native C++, #3

If you haven't read number one or two in this series, head back to http://einaros.livejournal.com/#einaros3782 before continuing.

Ok, so we've covered the motivation, as well as some of the requirements. It's time to give off an example of how the mechanism can be used. For the sake of utter simplicity, I will not bring classes and objects into the puzzle just yet. Just imagine the following simple console program

char globalBuffer[20];

DWORD WINAPI testThread(PVOID)
{
// Keep sleeping while the event is unset
while(WaitForSingleObjectEx(hExternalEvent, INFINITE, TRUE) != WAIT_OBJECT_0)
{
Sleep(10);
}

// Alter the global data
for(int i = 0; i < sizeof(globalBuffer) - 1; ++i)
{
globalBuffer[i] = 'b';
}
globalBuffer[sizeof(globalBuffer) - 1] = 0; // null terminate

// Return and terminate the thread
return 0;
}

int main()
{
DWORD dwThreadId;
CreateThread(NULL, 0, testThread, NULL, 0, &dwThreadId);
...
There's nothing out of the ordinary so far. We've got the entry point, main, and a function, testThread. When main is executed, it will create and spawn a new thread on testThread. All testThread does in this example, is to wait for an external event to be signaled, and then alter a data structure, globalBuffer. What's important is that the thread is waiting for something to happen, and while it's waiting we can instruct it to do some other stuff. Our objective is therefore to have the thread call another function, testFunction:

string testFunction(char c)
{
for(int i = 0; i < sizeof(globalBuffer) - 1; ++i)
{
globalBuffer[i] = c;
}
globalBuffer[sizeof(globalBuffer) - 1] = 0; // null terminate
return globalBuffer;
}
testfunction will alter the global buffer, setting all elements except the last to the value of the char parameter c, then null terminate it and finally return a new string with the global buffer's content. What we can tell straight away, is that testFunction and testThread may alter the same buffer. If our main thread executed testFunciton directly, it could get around to alter the first 10 or so elements of the global before being swapped out of the CPU. If the external event in testThread were to be signaled at this point, that thread would also start altering the buffer. The string returned from testFunction would obviously contain anything but what we expect it to.

While this example doesn't make much sense in terms of a real world application as it is, the concept is very much realistic. Imagine, if you wish, that the global buffer represents the text in an edit box within a dialog, and that testThread is supposed to alter this text based on a timer. At certain intervals, external threads may also wish to update the same edit box with additional information, so they call into the GUI's class (which in this simplistic example is represented by testFunction). To avoid crashes, garbled text in the text box, or other freaky results, we want to synchronize the access. We don't want to add a heap of mutexes or ciritcal sections to our code, but rather just have the GUI thread call the function which updates the text. When the GUI thread alone is in charge of updating its resources, we're guaranteed that all operations go about in a tidy order. In other words: there will be no headache-causing crashes and angry customers.

So, instead of adding a whole lot of interlocking code to both testThread and testFunction, which both update the global buffer, we use a cross thread call library to have the thread which owns the shared data do all the work.

int main()
{
DWORD dwThreadId;
CreateThread(NULL, 0, testThread, NULL, 0, &dwThreadId);

CallScheduler<APCPickupPolicy>* scheduler = CallScheduler<APCPickupPolicy>::getInstance();

try
{
string dataString = scheduler->syncCall<string>(dwThreadId, boost::bind(testFunction, 'a'), 500);
cout << "testFunction returned: " << dataString << endl;
}
catch(CallTimeoutException&)
{
cout << "Call timeout" << endl;
}
catch(CallSchedulingFailedException&)
{
cout << "Call scheduling failed" << endl;
}

return 0;
}

CallScheduler makes all the difference here. Through instantiating a reference to this singleton class, with the preferred pickup policy (in this case the APCPickupPolicy), we can schedule calls to be made in context of other threads, granted that the are open for whatever mechanism the pickup policy uses. In our current example, we know that the testThread wait is alertable, and that suits the APC policy perfectly. To attempt to execute the call in the other thread, we call the syncCall function, with a few parameters. The template parameter is the return type of the function we wish to execute, in this case a string. The first parameter is the id of the thread in which we wish to perform the operation, the second parameter is a boost functor, and the third is the number of milliseconds we are willing to wait for the call to be initiated. The use of boost functors also allows us to bind the parameters in a timely fashion. As you can see in the above call, testFunction should be called with the char 'a' as its sole parameter.

At this point, we wait. The call will be scheduled, and will hopefully completed. If the pickup policy does it's work, the call will be executed in the other thread, and we are soon to get a the string from testFunction as returned by syncCall. Should the pickup fail or timeout, an exception will be thrown. Consider the example -- it really should make it all pretty clear.

As for limitations, restrictions, guarantees in terms of reliability and how the framework is designed: I'll get back to that in the next update. Once again, stay tuned.

0 kommentarer: