Monday, 12 November 2012

Thread context in C++/CX apps

When developing Hilo we discovered that there are special context rules for task continuations that wrap async objects, or that use the create_async function. Therefore, in order to clarify our understanding of the thread context for our subroutines we developed three helper functions -  IsMainThread, IsBackgroundThread, and RecordMainThread. These functions allow clarification of whether a task continuation is running on the UI thread (main thread), or a thread pool thread (background thread).

The sample photo viewing app has been extended to include these helper functions, which are invoked in an assertion. In the Debug version of the sample app, these statements throw an exception if the thread being used is other than the one declared in the assertion.

Implementation

The IsMainThread, IsBackgroundThread, and RecordMainThread functions are shown below. The RecordMainThread function calls GetCurrentThreadId and stores the result in the mainThreadID variable. Then the IsMainThread function can be used to compare the value of the mainThreadId variable to the value returned by GetCurrentThreadId, and if they are identical, the function returns true. Similarly, the IsBackgroundThread function can be used to compare the value of the mainThreadId variable to the value returned by GetCurrentThreadId, and if they are not identical, the function returns true.

bool IsMainThread()
{
    return (mainThreadId == 0U || mainThreadId == GetCurrentThreadId());
}
 
bool IsBackgroundThread()
{
    return (mainThreadId == 0U || mainThreadId != GetCurrentThreadId());
}
 
void RecordMainThread()
{
    mainThreadId = GetCurrentThreadId();
}

The RecordMainThread function is invoked in the constructor of the App class, the entry point of the app. Note that the RecordMainThread function is only invoked if the build of the app is a Debug build.

App::App()
{
#ifndef NDEBUG
    RecordMainThread();
#endif
    InitializeComponent();
    Suspending += ref new SuspendingEventHandler(this, &App::OnSuspending);
}

The IsMainThread and IsBackgroundThread functions can then be wrapped in assertions which can be used to clarify your understanding of the thread context that your task continuations are running on.

ImageSource^ PhotoViewModel::Photo::get()
{
    if (nullptr == m_photo)
    {
        auto photoStream = make_shared<IRandomAccessStreamWithContentType^>(nullptr);
        auto backgroundContext = task_continuation_context::use_arbitrary();
 
        GetPhotoAsync().then([this](FileInformation^ file)
        {
            assert(IsBackgroundThread());
            if (file == nullptr)
            {
                cancel_current_task();
            }
            m_file = file;
            return file->OpenReadAsync();
        }, backgroundContext).then([photoStream](IRandomAccessStreamWithContentType^ imageData)
        {
            assert(IsBackgroundThread());
            (*photoStream) = imageData;
            return BitmapDecoder::CreateAsync(imageData);
        }, backgroundContext).then([this, photoStream](BitmapDecoder^ decoder)
        {
            assert(IsMainThread());
            m_photo = ref new WriteableBitmap(decoder->PixelWidth, decoder->PixelHeight);
            (*photoStream)->Seek(0);
            return m_photo->SetSourceAsync(*photoStream);
        }).then([this]()
        {
            assert(IsMainThread());
            if (m_isGreyscale)
            {
                ConvertPhotoToGreyscale(nullptr);
            }
            OnPropertyChanged("Photo");
        });
    }
    return m_photo;
}

In the getter of the Photo property in the PhotoViewModel class, the code asserts that the first continuation is running on a thread pool thread, rather than the UI thread. This is as expected as the continuation is declared to use an arbitrary thread from the thread pool. The second continuation also asserts that the continuation is running on on a thread pool thread, which is also as expected as the continuation is declared to use an arbitrary thread from the thread pool. The final two continuations assert that the continuations are running on the UI thread, which is also as expected because the continuations do not specify a thread to run on. The first two continuations run on thread pool threads in order to keep the UI responsive. The code then switches to running on the UI thread for the final two continuations because they both need to run on the UI thread as they update data on the UI.

Summary


In this blog post I’ve extended the sample photo viewing app to include functions that can be invoked in assertions to clarify the thread a task continuation is executing on. In the Debug version of the sample app, these statements throw an exception if the thread being used is other than the one declared in the assertion. This approach to clarifying thread context rules was discussed in a presentation at //build/ about Hilo, which can be viewed here.

The sample app can be downloaded here.

No comments:

Post a Comment