다음을 통해 공유


Async programming patterns and tips in Hilo (Windows Store apps using C++ and XAML)

From: Developing an end-to-end Windows Store app using C++ and XAML: Hilo

Previous page | Next page

Hilo contains many examples of continuation chains in C++, which are a common pattern for asynchronous programming in Windows Store apps. Here are some tips and guidance for using continuation chains, and examples of the various ways you can use them in your app.

Download

After you download the code, see Getting started with Hilo for instructions.

You will learn

  • Best practices for asynchronous programming in Windows Store apps using C++.
  • How to cancel pending asynchronous operations.
  • How to handle exceptions that occur in asynchronous operations.
  • How to use parallel tasks with Windows Store apps that are written in Visual C++.

Applies to

  • Windows Runtime for Windows 8
  • Visual C++ component extensions (C++/CX)
  • Parallel Patterns Library (PPL)

Ways to use the continuation chain pattern

The asynchronous programming techniques that we use in Hilo fall under the general pattern known as a continuation chain or a .then ladder (pronounced dot-then ladder). A continuation chain is a sequence of PPL tasks that are connected by data flow relationships. The output of each task becomes the input of the next continuation in the chain. The start of the chain is usually a task that wraps a Windows Runtime asynchronous operation (an interface that derives from IAsyncInfo). We introduced continuation chains in Writing modern C++ code for Windows Store apps in this guide.

Here’s an example from Hilo that shows a continuation chain that begins with task that wraps an IAsyncInfo-derived interface.

FileSystemRepository.cpp

task<IPhotoImage^> FileSystemRepository::GetSinglePhotoAsync(String^ photoPath)
{
    assert(IsMainThread());
    String^ query = "System.ParsingPath:=\"" + photoPath + "\"";    
    auto fileQuery = CreateFileQuery(KnownFolders::PicturesLibrary, query, IndexerOption::DoNotUseIndexer);
    shared_ptr<ExceptionPolicy> policy = m_exceptionPolicy;
    return create_task(fileQuery->GetFilesAsync(0, 1)).then([policy](IVectorView<StorageFile^>^ files) -> IPhotoImage^
    {
        if (files->Size > 0)
        {
            IPhotoImage^ photo = (ref new Photo(files->GetAt(0), ref new NullPhotoGroup(), policy))->GetPhotoImage();
            create_task(photo->InitializeAsync());
            return photo;
        }
        else
        {
            return nullptr;
        }
    }, task_continuation_context::use_current());
}

In this example, the expected calling context for the FileSystemRepository::GetSinglePhotoAsync method is the main thread. The method creates a PPL task that wraps the result of the GetFilesAsync method. The GetFilesAsync method is a helper function that returns an IAsyncOperation< IVectorView<StorageFile^>^>^ handle.

The task::then method creates continuations that run on the same thread or a different thread than the task that precedes them, depending on how you configure them. In this example, there is one continuation.

Note   Because the initial task that the concurrency::create_task function created wraps an IAsyncInfo-derived type, its continuations run by default in the context that creates the continuation chain, which in this example is the main thread.

 

Some operations, such as those that interact with XAML controls, must occur in the main thread. For example, the PhotoImage object can be bound to a XAML control and therefore must be instantiated in the main thread. On the other hand, you must call blocking operations from a background thread only and not your app’s main thread. So, to avoid programming errors, you need to know whether each continuation will use the main thread or a background thread from the thread pool.

There are a variety of ways to use the continuation chain pattern depending on your situation. Here are some variations of the basic continuation chain pattern:

  • Value-based and task-based continuations.
  • Unwrapped tasks.
  • Allowing continuation chains to be externally canceled.
  • Other ways of signaling cancellation.
  • Canceling asynchronous operations that are wrapped by tasks.
  • Using task-based continuations for exception handling.
  • Assembling the outputs of multiple continuations.
  • Using nested continuations for conditional logic.
  • Showing progress from an asynchronous operation.
  • Creating background tasks with create_async for interop scenarios.
  • Dispatching functions to the main thread.
  • Using the Asynchronous Agents Library.

Value-based and task-based continuations

The task that precedes a continuation is called the continuation’s antecedent task or antecedent. If an antecedent task is of type task<T>, a continuation of that task can either accept type T or type task<T> as its argument type. Continuations that accept type T are value-based continuations. Continuations that accept type task<T> are task-based continuations.

The argument of a value-based continuation is the return value of the work function of the continuation’s antecedent task. The argument of a task-based continuation is the antecedent task itself. You can use the task::get method to query for the output of the antecedent task.

There are several behavioral differences between value-based and task-based continuations. Value-based continuations don't run if the antecedent task terminated in an exception. In contrast, task-based continuations run regardless of the exception status of the antecedent task. Also, value-based continuations inherit the cancellation token of their antecedent by default while task-based continuations don't.

Most continuations are value-based continuations. You use task-based continuations in scenarios that involve exception handling and cancellation. See Allowing continuation chains to be externally canceled and Using task-based continuations for exception handling later in this topic for examples.

Unwrapped tasks

In most cases the work function of a PPL task returns type T if the task is of type task<T>, but returning type T is not the only possibility. You can also return a task<T> value from the work function that you pass to the create_task or task::then functions. In this case, you might expect PPL to create a task with type task<task<T>>, but this is not what happens. Instead, the resulting task has type task<T>. The automatic transformation of task<task<T>> to task<T> is called unwrapping a task.

Unwrapping occurs at the type level. PPL schedules the inner task and uses its result as the result of the outer task. The outer task finishes when the inner task completes its work.

Task 1 in the diagram has a work function that returns an int. The type of task 1 is task<int>. Task 2 has a work function that returns task<int>, which is task 2a in the diagram. Task 2 waits for task 2a to finish, and returns the result of task 2a's work function as the result of task 2.

Unwrapping tasks helps you put conditional dataflow logic in networks of related tasks. See Using nested continuations for conditional logic later on this page for scenarios and examples that require PPL to unwrap tasks. For a table of return types see "Lambda function return types and task return types" in Asynchronous programming in C++.

Allowing continuation chains to be externally canceled

You can create continuation chains that respond to external cancellation requests. For example, when you display the image browser page in Hilo, the page starts an asynchronous operation that creates groups of photos by month and displays them. If you navigate away from the page before the display operation has finished, the pending operation that creates month group controls is canceled. Here are the steps.

  1. Create a concurrency::cancellation_token_source object.
  2. Query the cancellation token source for a concurrency::cancellation_token using the cancellation_token_source::get_token method.
  3. Pass the cancellation token as an argument to the concurrency::create_task function of the initial task in the continuation chain or to the task::then method of any continuation. You only need to do this once for each continuation chain because subsequent continuation tasks use the cancellation context of their antecedent task by default.
  4. To cancel the continuation chain while it's running, call the cancel method of the cancellation token source from any thread context.

Here is an example.

ImageBrowserViewModel.cpp

void ImageBrowserViewModel::StartMonthAndYearQueries()
{
    assert(IsMainThread());
    assert(!m_runningMonthQuery);
    assert(!m_runningYearQuery);
    assert(m_currentMode == Mode::Active || m_currentMode == Mode::Running || m_currentMode == Mode::Pending);

    m_cancellationTokenSource = cancellation_token_source();
    auto token = m_cancellationTokenSource.get_token();
    StartMonthQuery(m_currentQueryId, token);
    StartYearQuery(m_currentQueryId, token);
}

The StartMonthAndYearQueries method creates the concurrency::cancellation_token_source and concurrency::cancellation_token objects and passes the cancellation token to the StartMonthQuery and StartYearQuery methods. Here is the implementation of StartMonthQuery method.

ImageBrowserViewModel.cpp

void ImageBrowserViewModel::StartMonthQuery(int queryId, cancellation_token token)
{
    m_runningMonthQuery = true;
    OnPropertyChanged("InProgress");
    m_photoCache->Clear();
    run_async_non_interactive([this, queryId, token]()
    {
        // if query is obsolete, don't run it.
        if (queryId != m_currentQueryId) return;

        m_repository->GetMonthGroupedPhotosWithCacheAsync(m_photoCache, token).then([this, queryId](task<IVectorView<IPhotoGroup^>^> priorTask)
        {
            assert(IsMainThread());
            if (queryId != m_currentQueryId)  
            {
                // Query is obsolete. Propagate exception and quit.
                priorTask.get();
                return;
            }

            m_runningMonthQuery = false;
            OnPropertyChanged("InProgress");
            if (!m_runningYearQuery)
            {
                FinishMonthAndYearQueries();
            }
            try
            {     
                // Update display with results.
                m_monthGroups->Clear();                   
                for (auto group : priorTask.get())
                {  
                    m_monthGroups->Append(group);
                }
                OnPropertyChanged("MonthGroups");
            }
            // On exception (including cancellation), remove any partially computed results and rethrow.
            catch (...)
            {
                m_monthGroups = ref new Vector<IPhotoGroup^>();
                throw;
            }
        }, task_continuation_context::use_current()).then(ObserveException<void>(m_exceptionPolicy));
    });
}

The StartMonthQuery method creates a continuation chain. The head of the continuation chain and the chain's first continuation task are constructed by the the FileSystemRepository class's GetMonthGroupedPhotosWithCacheAsync method.

FileSystemRepository.cpp

task<IVectorView<IPhotoGroup^>^> FileSystemRepository::GetMonthGroupedPhotosWithCacheAsync(shared_ptr<PhotoCache> photoCache, concurrency::cancellation_token token)
{
    auto queryOptions = ref new QueryOptions(CommonFolderQuery::GroupByMonth);
    queryOptions->FolderDepth = FolderDepth::Deep;
    queryOptions->IndexerOption = IndexerOption::UseIndexerWhenAvailable;
    queryOptions->Language = CalendarExtensions::ResolvedLanguage();
    auto fileQuery = KnownFolders::PicturesLibrary->CreateFolderQueryWithOptions(queryOptions);
    m_monthQueryChange = (m_imageBrowserViewModelCallback != nullptr) ? ref new QueryChange(fileQuery, m_imageBrowserViewModelCallback) : nullptr;

    shared_ptr<ExceptionPolicy> policy = m_exceptionPolicy;
    auto sharedThis = shared_from_this();
    return create_task(fileQuery->GetFoldersAsync()).then([this, photoCache, sharedThis, policy](IVectorView<StorageFolder^>^ folders) 
    {
        auto temp = ref new Vector<IPhotoGroup^>();
        for (auto folder : folders)
        {
            auto photoGroup = ref new MonthGroup(photoCache, folder, sharedThis, policy);
            temp->Append(photoGroup);
        }
        return temp->GetView();
    }, token);
}

This code passes a cancellation token to the task::then method at the first continuation task.

The year and month queries can take a few seconds to run if there are many pictures to process. It is possible that the user might navigate away from the page while they are running. If this happens the queries are cancelled. The OnNavigatedFrom method in the Hilo app calls the CancelMonthAndYearQueries method. The CancelMonthAndYearQueries of the ImageBrowserViewModel class invokes the cancel method of the cancellation token source.

ImageBrowserViewModel.cpp

void ImageBrowserViewModel::CancelMonthAndYearQueries()
{
    assert(m_currentMode == Mode::Running);

    if (m_runningMonthQuery)
    {
        m_runningMonthQuery = false;
        OnPropertyChanged("InProgress");
    }
    m_runningYearQuery = false;
    m_currentQueryId++;
    m_cancellationTokenSource.cancel();
}

Cancellation in PPL is cooperative. You can control the cancellation behavior of tasks that you implement. For the details about what happens when you call the cancellation token’s source cancel method, see "Canceling tasks" in Asynchronous programming in C++.

Other ways of signaling cancellation

A value-based continuation won't start if the cancel method of its associated cancellation token source has been called. It is also possible that the cancel method is called while a task in a continuation chain is running. If you want to know whether an external cancellation request has been signaled while running a task, you can call the concurrency::is_task_cancellation_requested function to find out. The is_task_cancellation_requested function checks the status of the current task’s cancellation token. The function returns true if the cancel method has been called on the cancellation token source object that created the cancellation token. (For an example see Using C++ in the Bing Maps Trip Optimizer sample.)

Note  You can call is_task_cancellation_requested from within the body of the work function for any kind of task, including tasks created by create_task function, the create_async function and task::then method.

 

Tip  Don't call is_task_cancellation_requested within every iteration of a tight loop. In general, if you want to detect a cancellation request from within in a running task, poll for cancellation frequently enough to give you the response time you want without affecting your app's performance when cancellation is not requested.

 

After you detect that a running task must process a cancellation request, perform whatever cleanup your app needs and then call the concurrency::cancel_current_task function to end your task and notify the runtime that cancellation has occurred.

Cancellation tokens are not the only way to signal cancellation requests. When you call asynchronous library functions, you sometimes receive a special signal value from the antecedent task, such as nullptr, to indicate that a cancellation was requested.

Canceling asynchronous operations that are wrapped by tasks

PPL tasks help you provide cancellation support in apps that use the asynchronous operations of the Windows Runtime. When you wrap an IAsyncInfo-derived interface in a task, PPL notifies the asynchronous operation (by calling the IAsyncInfo::Cancel method) if a cancellation request arrives.

Note  PPL's concurrency::cancellation_token is a C++ type that can't cross the ABI. This means that you can't pass it as an argument to a public method of a public ref class. If you need a cancellation token in this situation, wrap the call to your public ref class's public async method in a PPL task that uses a cancellation token. Then, call the concurrency::is_task_cancellation_requested function within the work function of the create_async function inside the async method. The is_task_cancellation_requested function has access to the PPL task’s token, and returns true if the token has been signaled.

 

Using task-based continuations for exception handling

PPL tasks support deferred exception handling. You can use a task-based continuation to catch exceptions that occurred in any of the steps of a continuation chain.

In Hilo we check for exceptions systematically, at the end of every continuation chain. To make this easy, we created a helper class that looks for exceptions and handles them according to a configurable policy. Here's the code.

TaskExceptionsExtensions.h

    template<typename T>
    struct ObserveException
    {
        ObserveException(std::shared_ptr<ExceptionPolicy> handler) : m_handler(handler)
        {
        }

        concurrency::task<T> operator()(concurrency::task<T> antecedent) const
        {
            T result;
            try 
            {
                result = antecedent.get();
            }
            catch(const concurrency::task_canceled&)
            {
                // don't need to run canceled tasks through the policy
            }
            catch(const std::exception&)
            {
                auto translatedException = ref new Platform::FailureException();
                m_handler->HandleException(translatedException);
                throw;
            }
            catch(Platform::Exception^ ex)
            {
                m_handler->HandleException(ex);
                throw;
            }
            return antecedent;
        }

    private:
        std::shared_ptr<ExceptionPolicy> m_handler;
    };

Assembling the outputs of multiple continuations

Continuation chains use a dataflow style of programming where the return value of an antecedent task becomes the input of a continuation task. In some situations the return value of the antecedent task doesn't contain everything you need for the next step. For example, you might need to merge the results of two continuation tasks before a third continuation can proceed. There are several techniques for handling this case. In Hilo we use a shared pointer (std::shared_ptr) to temporarily hold intermediate values on the heap.

For example, the next diagram shows the dataflow and control flow relationships of Hilo’s ThumbnailGenerator::CreateThumbnailFromPictureFileAsync method, which uses a continuation chain to create thumbnail images. Creating a thumbnail from a picture file requires asynchronous steps that don’t fit a straight-line dataflow pattern.

In the diagram, the solid ovals represent asynchronous operations in the Windows Runtime. The dashed ovals are tasks that call synchronous functions. The arcs represent inputs and outputs. The dashed arrows are control flow dependencies for operations with side effects. The numbers show the order in which the operations occur in the CreateThumbnailFromPictureFileAsync method’s continuation chain.

The diagram shows interactions that are too complex for linear dataflow. For example, the synchronous "Set pixel data" operation requires inputs from three separate asynchronous operations, and it modifies one of its inputs, which causes a sequencing constraint for a subsequent asynchronous flush operation. Here's the code for this example.

ThumbnailGenerator.cpp

task<InMemoryRandomAccessStream^> ThumbnailGenerator::CreateThumbnailFromPictureFileAsync(
    StorageFile^ sourceFile, 
    unsigned int thumbSize)
{
    (void)thumbSize; // Unused parameter
    auto decoder = make_shared<BitmapDecoder^>(nullptr);
    auto pixelProvider = make_shared<PixelDataProvider^>(nullptr);
    auto resizedImageStream = ref new InMemoryRandomAccessStream();
    auto createThumbnail = create_task(
        sourceFile->GetThumbnailAsync(
        ThumbnailMode::PicturesView, 
        ThumbnailSize));

    return createThumbnail.then([](StorageItemThumbnail^ thumbnail)
    {
        IRandomAccessStream^ imageFileStream = 
            static_cast<IRandomAccessStream^>(thumbnail);

        return BitmapDecoder::CreateAsync(imageFileStream);

    }).then([decoder](BitmapDecoder^ createdDecoder)
    {
        (*decoder) = createdDecoder;
        return createdDecoder->GetPixelDataAsync( 
            BitmapPixelFormat::Rgba8,
            BitmapAlphaMode::Straight,
            ref new BitmapTransform(),
            ExifOrientationMode::IgnoreExifOrientation,
            ColorManagementMode::ColorManageToSRgb);

    }).then([pixelProvider, resizedImageStream](PixelDataProvider^ provider)
    {
        (*pixelProvider) = provider;
        return BitmapEncoder::CreateAsync(
            BitmapEncoder::JpegEncoderId, 
            resizedImageStream);

    }).then([pixelProvider, decoder](BitmapEncoder^ createdEncoder)
    {
        createdEncoder->SetPixelData(BitmapPixelFormat::Rgba8,
            BitmapAlphaMode::Straight,
            (*decoder)->PixelWidth,
            (*decoder)->PixelHeight,
            (*decoder)->DpiX,
            (*decoder)->DpiY,
            (*pixelProvider)->DetachPixelData());
        return createdEncoder->FlushAsync();

    }).then([resizedImageStream]
    {
        resizedImageStream->Seek(0);
        return resizedImageStream;
    });
}

The example calls the std::make_shared function to create shared containers for handles to the decoder and pixel provider objects that will be created by the continuations. The * (dereference) operator allows the continuations to set the shared containers and read their values.

Using nested continuations for conditional logic

Nested continuations let you defer creating parts of a continuation chain until run time. Nested continuations are helpful when you have conditional tasks in the chain and you need to capture local variables of tasks in a continuation chain for use in subsequent continuation tasks. Nested continuations rely on task unwrapping.

Hilo’s image rotation operation uses nested continuations to handle conditional logic. The rotation operation behaves differently depending on whether the file format of the image being rotated supports the Exchangeable Image File Format (EXIF) orientation property. Here's the code.

RotateImageViewModel.cpp

concurrency::task<BitmapEncoder^> RotateImageViewModel::SetEncodingRotation(BitmapEncoder^ encoder, shared_ptr<ImageEncodingInformation> encodingInfo, float64 rotationAngle, concurrency::task_continuation_context backgroundContext)
{
    // If the file format supports Exif orientation then update the orientation flag
    // to reflect any user-specified rotation. Otherwise, perform a hard rotate 
    // using the BitmapTransform class.
    auto encodingTask = create_empty_task();
    if (encodingInfo->usesExifOrientation)
    {
        // Try encoding with Exif with updated values.
        auto currentExifOrientationDegrees = ExifExtensions::ConvertExifOrientationToDegreesRotation(ExifRotations(encodingInfo->exifOrientation));
        auto newRotationAngleToApply = CheckRotationAngle(safe_cast<unsigned int>(rotationAngle + currentExifOrientationDegrees));
        auto exifOrientationToApply = ExifExtensions::ConvertDegreesRotationToExifOrientation(newRotationAngleToApply);
        auto orientedTypedValue = ref new BitmapTypedValue(static_cast<unsigned short>(exifOrientationToApply), PropertyType::UInt16);

        auto properties = ref new Map<String^, BitmapTypedValue^>();
        properties->Insert(EXIFOrientationPropertyName, orientedTypedValue);

        encodingTask = encodingTask.then([encoder, properties]
        { 
            assert(IsBackgroundThread());
            return encoder->BitmapProperties->SetPropertiesAsync(properties);
        }, backgroundContext).then([encoder, encodingInfo] (task<void> setPropertiesTask)
        {
            assert(IsBackgroundThread());

            try 
            {
                setPropertiesTask.get();
            }
            catch(Exception^ ex)
            {
                switch(ex->HResult)
                {
                case WINCODEC_ERR_UNSUPPORTEDOPERATION:
                case WINCODEC_ERR_PROPERTYNOTSUPPORTED:
                case E_INVALIDARG:
                    encodingInfo->usesExifOrientation = false;
                    break;
                default:
                    throw;
                }
            }

        }, backgroundContext);
    }

    return encodingTask.then([encoder, encodingInfo, rotationAngle]
    {
        assert(IsBackgroundThread());
        if (!encodingInfo->usesExifOrientation)
        {
            BitmapRotation rotation = static_cast<BitmapRotation>((int)floor(rotationAngle / 90));
            encoder->BitmapTransform->Rotation = rotation;
        }
        return encoder;
    });
}

Showing progress from an asynchronous operation

Although the Hilo app doesn't report the progress of ongoing operations using a progress bar, you may want to. To learn more, see the blog post Keeping apps fast and fluid with asynchrony in the Windows Runtime, which has some examples of how to use the IAsyncActionWithProgress<TProgress> and the IAsyncOperationWithProgress<TProgress, TResult> interfaces.

Note  Hilo shows the animated progress ring control during long-running asynchronous operations. For more info about how Hilo displays progress, see ProgressRing in this guide.

 

Creating background tasks with create_async for interop scenarios

Sometimes you must directly use the interfaces that are derived from IAsyncInfo in classes that you define. For example, the signatures of public methods of public ref classes in your app cannot include non-ref types such as task<T>. Instead, you expose asynchronous operations with one of the interfaces that derive from the IAsyncInfo interface. (A public ref class is a C++ class that has been declared with public visibility and the C++/CX ref keyword.)

You can use the concurrency::create_async function to expose a PPL task as an IAsyncInfo object.

See Creating Asynchronous Operations in C++ for Windows Store apps for more info about the create_async function.

Dispatching functions to the main thread

The continuation chain pattern works well for operations that the user initiates by interacting with XAML controls. Most operations begin on the main thread with an invocation of a view model property that is bound to a property of the XAML control. The view model creates a continuation chain that runs some tasks on the main thread and some tasks in the background. Updating the UI with the result of the operation takes place on the main thread.

But not all updates to the UI are in response to operations that begin on the main thread. Some updates can come from external sources, such as network packets or devices. In these situations, your app may need to update XAML controls on the main thread outside of a continuation context. A continuation chain might not be what you need.

There are two ways to handle this situation. One way is to create a task that does nothing and then schedule a continuation of that task that runs in the main thread using the result of the concurrency::task_continuation_context::use_current function as an argument to the task::then method. This approach makes it easy to handle exceptions that arise during the operation. You can handle exceptions with a task-based continuation. The second option is familiar to Win32 programmers. You can use the CoreDispatcher class’s RunAsync method to run a function in the main thread. You can get access to a CoreDispatcher object from the Dispatcher property of the CoreWindow object. If you use the RunAsync method, you need to decide how to handle exceptions. By default, if you invoke RunAsync and ignore its return value, any exceptions that occur during the operation will be lost. If you don't want to lose exceptions you can wrap the IAsyncAction^ handle that the RunAsync method returns with a PPL task and add a task-based continuation that observes any exceptions of the task.

Here's an example from Hilo.

TaskExtensions.cpp

void run_async_non_interactive(std::function<void ()>&& action)
{
    Windows::UI::Core::CoreWindow^ wnd = Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow;
    assert(wnd != nullptr);

    wnd->Dispatcher->RunAsync(
        Windows::UI::Core::CoreDispatcherPriority::Low, 
        ref new Windows::UI::Core::DispatchedHandler([action]()
    {
        action();
    })); 
} 

This helper function runs functions that are dispatched to the main thread at a low priority, so that they don't compete with actions on the UI.

Using the Asynchronous Agents Library

Agents are a useful model for parallel programming, especially in stream-oriented apps such as UIs. Use message buffers to interact with agents. For more info see Asynchronous Agents Library.

[Top]

Tips for async programming in Windows Store apps using C++

Here are some tips and guidelines that can help you write effective asynchronous code in Windows Store apps that use C++.

  • Don’t program with threads directly.
  • Use "Async" in the name of your async functions.
  • Wrap all asynchronous operations of the Windows Runtime with PPL tasks.
  • Return PPL tasks from internal async functions within your app.
  • Return IAsyncInfo-derived interfaces from public async methods of public ref classes.
  • Use public ref classes only for interop.
  • Use modern, standard C++, including the std namespace.
  • Use task cancellation consistently.
  • Handle task exceptions using a task-based continuation.
  • Handle exceptions locally when using the when_all function.
  • Call view model objects only from the main thread.
  • Use background threads whenever possible.
  • Don't call blocking operations from the main thread.
  • Don't call task::wait from the main thread.
  • Be aware of special context rules for continuations of tasks that wrap async objects.
  • Be aware of special context rules for the create_async function.
  • Be aware of app container requirements for parallel programming.
  • Use explicit capture for lambda expressions.
  • Don't create circular references between ref classes and lambda expressions.
  • Don't use unnecessary synchronization.
  • Don't make concurrency too fine-grained.
  • Watch out for interactions between cancellation and exception handling.
  • Use parallel patterns.
  • Be aware of special testing requirements for asynchronous operations.
  • Use finite state machines to manage interleaved operations.

Don’t program with threads directly

Although Windows Store apps cannot create new threads directly, you still have the full power of C++ libraries such as the PPL and the Asynchronous Agents Library. In general, use features of these libraries for all new code instead of programming with threads directly.

Note  When porting existing code, you can continue to use thread pool threads. See Thread pool sample for a code example.

 

Use "Async" in the name of your async functions

When you write a function or method that operates asynchronously, use "Async" as part of its name to make this immediately apparent. All async functions and methods in Hilo use this convention. The Windows Runtime also uses it for all async functions and methods.

Wrap all asynchronous operations of the Windows Runtime with PPL tasks

In Hilo, we wrap every async operation that we get from the Windows Runtime in a PPL task by using the concurrency::create_task function. We found that PPL tasks are more convenient to work with than the lower-level async interfaces of the Windows Runtime. Unless you use PPL (or write custom wrapping code that checks HRESULT values), your app will ignore any errors that occur during the async operations of the Windows Runtime. In contrast, an async operation that is wrapped with a task will automatically propagate runtime errors by using PPL deferred exceptions.

Also, PPL tasks give you an easy-to-use syntax for the continuation chain pattern.

The only exception to this guidance is the case of fire-and-forget functions where you don't want notification of exceptions, which is an unusual situation.

Return PPL tasks from internal async functions within your app

We recommend that you return PPL tasks from all asynchronous functions and methods in your app, unless they are public, protected, or public protected methods of public ref classes. The IAsyncInfo-derived interfaces only for public async methods of public ref classes.

Return IAsyncInfo-derived interfaces from public async methods of public ref classes

The compiler requires that any public, protected or public protected async methods of your app’s public ref classes return an IAsyncInfo type. The actual interface must be a handle to one of the 4 interfaces that derive from the IAsyncInfo interface:

In general, apps will expose public async methods in public ref classes only in cases of application binary interface (ABI) interop.

Use public ref classes only for interop

Use public ref classes for interop across the ABI. For example, you must use public ref classes for view model classes that expose properties for XAML data binding.

Don't declare classes that are internal to your app using the ref keyword. There are cases where this guidance doesn’t apply, for example, when you are creating a private implementation of an interface that must be passed across the ABI. In this situation, you need a private ref class. Such cases are rare.

Similarly, we recommend that you use the data types in the Platform namespace such as Platform::String primarily in public ref classes and to communicate with Windows Runtime functions. You can use std::wstring and wchar_t * within your app and convert to a Platform::String^ handle when you cross the ABI.

Use modern, standard C++, including the std namespace

Windows Store apps should use the latest C++ coding techniques, standards and libraries. Use a modern programming style, and include the data types operations of the std namespace. For example, Hilo uses the std::shared_ptr and std::vector types.

Only the outermost layer of your app needs to use C++/CX. This layer interacts with XAML across the ABI and perhaps with other languages such as JavaScript or C#.

Use task cancellation consistently

There are several ways to implement cancellation by using PPL tasks and IAsyncInfo objects. It's a good idea to choose one way and use it consistently throughout your app. A consistent approach to cancellation helps to avoid coding mistakes and makes code reviews easier. Your decision is a matter of taste, but here’s what worked well for Hilo.

  • We determine support for external token-based cancellation for each continuation chain and not for each task in a continuation chain. In other words, if the first task in a continuation supports external cancellation using a cancellation token, then all tasks in that chain, including nested tasks, support token-based cancellation. If the first task doesn't support token-based cancellation, then no task in the chain supports token-based cancellation.
  • We create a concurrency::cancellation_token_source object whenever we start building a new continuation chain that needs to support cancellation that is initiated outside of the continuation chain itself. For example, in Hilo we use cancellation token source objects to allow pending async operations to be canceled when the user navigates away from the current page.
  • We call the get_token method of the cancellation token source. We pass the cancellation token as an argument to the create_task function that creates the first task in the continuation chain. If there are nested continuations, we pass the cancellation token to the first task of each nested continuation chain. We don't pass a cancellation token to the task::then invocations that create continuations. Value-based continuations inherit the cancellation token of their antecedent task by default and don't need a cancellation token argument to be provided. Task-based continuations don't inherit cancellation tokens, but they generally perform cleanup actions that are required whether or not cancellation has occurred.
  • When long-running tasks can be canceled, we call the concurrency::is_task_cancellation_requested function periodically to check if a request for cancellation is pending. Then, after performing cleanup actions, we call the concurrency::cancel_current_task function to exit the task and propagate the cancellation along the continuation chain.
  • When an antecedent task uses alternative signaling techniques to indicate cancellation, such as returning nullptr, we call the cancel_current_task function in the continuation to propagate the cancellation along the continuation chain. Sometimes receiving a null pointer from the antecedent task is the only mechanism for cancelling a continuation chain. In other words, not all continuation chains can be canceled externally. For example, in Hilo, we sometimes cancel a continuation chain when the user selects the Cancel button of the file picker. When this happens the async file picking operation returns nullptr.
  • We use a try/catch block in a task-based continuation whenever we need to detect whether a previous task in a continuation chain was canceled. In this situation, we catch the concurrency::task_canceled exception.
  • There is a try/catch block in Hilo’s ObserveException function object that prevents the task_canceled exception from being unobserved. This stops the app from being terminated when a cancellation occurs and no other specific handling for the exception exists.
  • If a Hilo component creates a cancelable task on behalf of another part of the app, we make sure that the task-creating function accepts a cancellation_token as an argument. This allows the caller to specify the cancellation behavior.

Handle task exceptions using a task-based continuation

It’s a good idea to have a continuation at the end of each continuation chain that catches exceptions with a final lambda. We recommend only catching exceptions that you know how to handle. Other exceptions will be caught by the runtime and cause the app to terminate using the "fail fast" std::terminate function.

In Hilo, every continuation chain ends with a continuation that calls Hilo’s ObserveException function object. See Using task-based continuations for exception handling on this page for more info and a code walkthrough.

Handle exceptions locally when using the when_all function

The when_all function returns immediately when any of its tasks throws an exception. If this happens, some of the tasks may still be running. When an exception occurs, your handler must wait for the pending tasks.

To avoid the complexity of having to test for exceptions and wait for tasks to complete, handle exceptions locally within the tasks that are managed by when_all.

For example, Hilo uses when_all for loading thumbnail images. The exceptions are handled in the tasks and not propagated back to when_all. Hilo passes a configurable exception policy object to the tasks to ensure that the tasks can use the app’s global exception policy. Here's the code.

ThumbnailGenerator.cpp

task<Vector<StorageFile^>^> ThumbnailGenerator::Generate( 
    IVector<StorageFile^>^ files, 
    StorageFolder^ thumbnailsFolder)
{
    vector<task<StorageFile^>> thumbnailTasks;

    unsigned int imageCounter = 0;
    for (auto imageFile : files)
    {
        wstringstream localFileName;
        localFileName << ThumbnailImagePrefix << imageCounter++ << ".jpg";

        thumbnailTasks.push_back(
            CreateLocalThumbnailAsync(
            thumbnailsFolder,
            imageFile, 
            ref new String(localFileName.str().c_str()),
            ThumbnailSize,
            m_exceptionPolicy));
    }

    return when_all(begin(thumbnailTasks), end(thumbnailTasks)).then(
        [](vector<StorageFile^> files)
    {
        auto result = ref new Vector<StorageFile^>();
        for (auto file : files)
        {
            if (file != nullptr)
            {
                result->Append(file);
            }
        }

        return result;
    });
}

Here's the code for each task.

ThumbnailGenerator.cpp

task<StorageFile^> ThumbnailGenerator::CreateLocalThumbnailAsync(
    StorageFolder^ folder,
    StorageFile^ imageFile,
    String^ localFileName,
    unsigned int thumbSize,
    std::shared_ptr<ExceptionPolicy> exceptionPolicy)
{
    auto createThumbnail = create_task(
        CreateThumbnailFromPictureFileAsync(imageFile, thumbSize));

    return createThumbnail.then([exceptionPolicy, folder, localFileName](
        task<InMemoryRandomAccessStream^> createdThumbnailTask) 
    {
        InMemoryRandomAccessStream^ createdThumbnail;
        try 
        {
            createdThumbnail = createdThumbnailTask.get();
        }
        catch(Exception^ ex)
        {
            exceptionPolicy->HandleException(ex);
            // If we have any exceptions we won't return the results
            // of this task, but instead nullptr.  Downstream 
            // tasks will need to account for this.
            return create_task_from_result<StorageFile^>(nullptr);
        }

        return InternalSaveToFile(folder, createdThumbnail, localFileName);
    });
}

Call view model objects only from the main thread

Some methods and properties of view model objects are bound to XAML controls using data binding. When the UI invokes these methods and properties, it uses the main thread. Also, by convention, call all methods and properties of view model objects on the main thread.

Use background threads whenever possible

If the first task in a continuation chain wraps an asynchronous operation of the Windows Runtime, then by default the continuations run in the same thread as the task::then method. This is usually the main thread. This default is appropriate for operations that interact with XAML controls, but is not the right choice for many kinds of app-specific processing, such as applying image filters or performing other compute-intensive operations.

Be sure to use the value that the task_continuation_context::use_arbitrary function returns as an argument to the task::then method when you create continuations that should run in the background on thread pool threads.

Don't call blocking operations from the main thread

Don't call long-running operations in the main thread. In general, a good rule of thumb is to not call user-written functions that block the main thread for more than 50 milliseconds at a time. If you need to call a long-running function, wrap it in a task that runs in the background.

Note  This tip concerns only the amount of time that you block the main thread, not the time that is required to complete an async operation. If an aysnc operation is very long, give the user a visual indication of its progress while keeping the main thread unblocked.

 

For more info see Keep the UI thread responsive (Windows Store apps using C#/VB/C++ and XAML).

Don't call task::wait from the main thread

Don't call the task::wait method from the main thread (or any STA thread). If you need to do something in the main thread after a PPL task completes, use the task::then method to create a continuation task.

Be aware of special context rules for continuations of tasks that wrap async objects

Windows Store apps using C++ use the /ZW compiler switch that affects the behavior of several functions. If you use these functions for desktop or console applications, be aware of the differences caused by whether you use the switch or not.

With the /ZW switch, when you invoke the task::then method on a task that wraps an IAsyncInfo object, the default run-time context of the continuation is the current context. The current context is the thread that invoked the then method. This default applies to all continuations in a continuation chain, not just the first one. This special default for Windows Store apps makes coding easier. In most cases continuations of Windows Runtime operations need to interact with objects that require you to run from the app’s main thread. For an example see Ways to use the continuation chain pattern on this page.

When a task doesn't wrap one of the IAsyncInfo interfaces, the then method produces a continuation that runs by default on a thread that the system chooses from the thread pool.

You can override the default behavior by passing an additional continuation context parameter to the task::then method. In Hilo, we always provide this optional parameter when the first task of the continuation chain has been created by a separate component or a helper method of the current component.

Note  In Hilo, we found it useful to clarify our understanding of the thread context for our subroutines by using assert(IsMainThread()) and assert(IsBackgroundThread()) statements. In the Debug version of Hilo, these statements throw an exception if the thread being used is other than the one declared in the assertion. The implementations of the IsMainThread and IsBackgroundThread functions are in the Hilo source.

 

Be aware of special context rules for the create_async function

With the /ZW switch, the ppltasks.h header file provides the concurrency::create_async function that allows you to create tasks that are automatically wrapped by one of the IAsyncInfo interfaces of the Windows Runtime. The create_task and create_async functions have similar syntax and behavior, but there are some differences between them.

When you pass a work function as an argument to the create_async function, that work function can return void, an ordinary type T, task<T>, or a handle to any of the IAsyncInfo-derived interfaces such as IAsyncAction^ and IAsyncOperation<T>^.

When you call the create_async function, the work function runs synchronously in the current context or asynchronously in the background, depending on the return type of the work function. If the return type of the work function is void or an ordinary type T, the work function runs in the background on a thread-pool thread. If the return type of the work function is a task or a handle to one of the interfaces that derive from IAsyncInfo, then the work function runs synchronously in the current thread. Running the work function synchronously is helpful in cases where it is a small function that does a small amount of set up before invoking another async function.

Note  Unlike the create_async function, when you call the create_task function, it always runs in the background on a thread pool thread, regardless of the return type of the work function.

 

Note  In Hilo, we found it useful to clarify our understanding of the thread context for our subroutines by using assert(IsMainThread()) and assert(IsBackgroundThread()) statements. In the Debug version of Hilo, these statements throw an exception if the thread being used is other than the one declared in the assertion. The implementations of the IsMainThread and IsBackgroundThread functions are in the Hilo source.

 

Be aware of app container requirements for parallel programming

You can use the PPL and the Asynchronous Agents Library in a Windows Store app, but you can’t use the Concurrency Runtime’s Task Scheduler or the Resource Manager components.

Use explicit capture for lambda expressions

It’s a good idea to be explicit about the variables you capture in lambda expressions. For that reason we don’t recommend that you use the [=] or [&] options for lambda expressions.

Also, be aware of the object lifetime when you capture variables in lambda expressions. To prevent memory errors, don’t capture by reference any variables that contain stack-allocated objects. In addition, don’t capture member variables of transient classes, for the same reason.

Don't create circular references between ref classes and lambda expressions

When you create a lambda expression and capture the this handle by value, the reference count of the this handle is incremented. If you later bind the newly created lambda expression to a member variable of the class referenced by this, you create a circular reference that can result in a memory leak.

Use weak references to capture the this reference when the resulting lambda would create a circular reference. For a code example see Weak references and breaking cycles (C++/CX).

Don't use unnecessary synchronization

The programming style of Windows Store apps can make some forms of synchronization, such as locks, needed less often. When multiple continuation chains are running at the same time, they can easily use the main thread as a synchronization point. For example, the member variables of view model objects are always accessed from the main thread, even in a highly asynchronous app, and therefore you don’t need to synchronize them with locks.

In Hilo, we are careful to access member variables of view model classes only from the main thread.

Don't make concurrency too fine-grained

Use concurrent operations only for long-running tasks. There is a certain amount of overhead in setting up an asynchronous call.

Watch out for interactions between cancellation and exception handling

PPL implements some parts of its task cancellation functionality by using exceptions. If you catch exceptions that you don’t know about, you may interfere with PPL's cancellation mechanism.

Note  If you only use cancellation tokens to signal cancellation and don't call the cancel_current_task function, no exceptions will be used for cancellation.

 

Note  If a continuation needs to check for cancellations or exceptions that occurred in tasks earlier in the continuation chain, make sure that the continuation is a task-based continuation.

 

Use parallel patterns

If your app performs heavy computations it is very likely that you need to use parallel programming techniques. There are a number of well-established patterns for effectively using multicore hardware. Parallel Programming with Microsoft Visual C++ is a resource for some of the most common patterns, with examples that use PPL and the Asynchronous Agents Library.

Be aware of special testing requirements for asynchronous operations

Unit tests require special handling for synchronization when there are async calls. Most testing frameworks don't allow tests to wait for asynchronous results. You need special coding to work around this issue.

In addition, when you test components that require some operations to occur in the main thread, you need a way to make sure the testing framework executes in the required thread context.

In Hilo we addressed both of these issues with custom testing code. See Testing the app in this guide for a description of what we did.

Use finite state machines to manage interleaved operations

Async programs have more possible execution paths and potential feature interactions than synchronous programs. For example, if your app allows the user to press the back button while an async operation such as loading a file is in progress, you must account for this in your app’s logic. You might cancel the pending operation and restore the app’s state to what it was before the operation started.

Allowing your app’s operations to be interleaved in a flexible way helps increase the user’s perception of the app’s speed and responsiveness. It is an important characteristic of Windows Store apps, but if you’re not careful it can be a source of bugs.

Correctly handling the many execution paths and interactions that are possible with an async UI requires special programming techniques. Finite state machines are a good choice that can help you implement robust handling of interleaved operations of async operations. With a finite state machine, you explicitly describe what happens in each possible situation.

Here is a diagram of a finite state machine from Hilo’s image browser view model.

In the diagram, each oval represents a distinct operational mode that an instance of the ImageBrowserViewModel class can have at run time. The arcs are named transitions between modes. Some state transitions occur when an async operation finishes. You can recognize them because they contain the word Finish in their name. All of the others occur when an async operation starts.

The diagram shows what can happen in each operational mode. For example, the if the image browser is in the Running mode and an OnNavigatedFrom action occurs, then the resulting mode is Pending. Transitions are the result of

  • user actions such as navigation requests
  • program actions such as the completion of a previously started async operation
  • external events such as notifications of network or file system events

Not all transitions are available in each mode. For example, the transition labeled FinishMonthAndYearQueries can occur only in the Running mode.

In the code, the named transitions are member functions of the ImageBrowserViewModel class. The image browser’s current operational mode is the value of the m_currentMode member variable, which stores values of the ImageBrowserViewModel::Mode enumeration.

ImageBrowserViewModel.h

enum class Mode {
    Default,        /* (0, 0, 0): no pending data changes, not updating, not visible */
    Active,         /* (0, 0, 1): no pending data changes, not updating, visible */
    Pending,        /* (1, 0, 0): pending data changes, not updating, not visible */
    Running,        /* (0, 1, 1): no pending data changes, updating, visible  */    
    NotAllowed      /* error state */
};

Here is an example of the code that implements the ObserveFileChange transition in the diagram.

ImageBrowserViewModel.cpp

// State transition occurs when file system changes invalidate the result of the current query.
void ImageBrowserViewModel::ObserveFileChange()
{
    assert(IsMainThread());

    switch (m_currentMode)
    {
    case Mode::Default:
        m_currentMode = Mode::Pending;
        break;

    case Mode::Pending:
        m_currentMode = Mode::Pending;
        break;

    case Mode::Active:
        StartMonthAndYearQueries();
        m_currentMode = Mode::Running;
        break;

    case Mode::Running:
        CancelMonthAndYearQueries();
        StartMonthAndYearQueries();
        m_currentMode = Mode::Running;
        break;
    }
}

The ObserveFileChange method responds to operating system notifications of external changes to the image files that the user is viewing. There are 4 cases to consider, depending on what is happening in the app at the time of notification. For example, if the image browser is currently running an async query, its mode is Running. In this mode, the ObserveFileChange action cancels the running query because the results are no longer needed and then starts a new query. The operational mode remains Running.

[Top]