다음을 통해 공유


Windows 8 Asynchrony with PPL

Living with Latency

Our customers demand more speed and responsiveness from applications. At the same time, as the applications are becoming more and more connected, we are increasingly confronted with the latency inherent in I/O devices.

A thread performing a blocking I/O operation effectively delegates control to the hardware device performing that operation. Depending on the nature of the operation, and the ambient conditions such as the network throughput, the operation may complete right away or after a considerable delay. Because such conditions are unpredictable, an application that is fast and responsive when run on the developer’s desktop machine can be infuriatingly slow on the customer’s laptop or on a server in a datacenter.

A technique well-known to web developers – AJAX – relies on issuing multiple asynchronous requests, each associated with its own completion event, as in the example below:

 http.open("GET", "customer.html");
 http.onreadystatechange = function() {
   if(http.readyState == 4) {
     var serverResponse = http.responseText;
     // process serverResponse here:
     ...
   }
 }
 http.send(null);

The key to avoiding a blocked thread is to divorce the start of the operation from its completion. The main thread can then carry on performing other tasks with the completion fired at a later time.

This approach has also been used successfully in .NET, where it is known as the Event-based Asynchronous Pattern.

With Windows 8, you can write Metro style apps using the Windows Runtime asynchronous operations, which are built on the same principle.

Introducing Windows Runtime Asynchronous Operations

The Windows Runtime allows developers to build responsive applications using the new asynchronous programming model that is based on the idea of separating the start of the operation from its completion.

First, we’ll look at an example of how you program against the Windows Runtime directly to create a file in the standard “Pictures” folder. You will notice a few new keywords like ref new that are part of the C++ language extensions Microsoft introduced to support Windows Runtime. There is a lot more to say on this, but that’s not the subject of this post – so if you’re hungry for information, I recommend this Channel9 talk by Herb Sutter delivered recently at the //BUILD/ conference, or this or this in-depth talks from our C++ compiler experts.

In the meantime, here goes the example:

 StorageFolder^ item = KnownFolders::PicturesLibrary;
 StorageFileRetrievalOperation^ createStorageFileOp = 
   item->CreateFileAsync("myfile.txt");
  
 createStorageFileOp->Completed = ref new
   AsyncOperationCompletedHandler<StorageFile^>(
     [](IAsyncOperation<StorageFile^>^ asyncOp)
 {
     StorageFile^ storageFile = asyncOp->GetResults();
 });
 createStorageFileOp.Start();

First, you obtain a reference to the “Pictures” folder and then invoke the CreateFileAsync method on it. Notice the “Async” suffix in the name of the method – this method does not yet create a file but only starts an operation that will create a file. That operation will need to be started, but before you do that you need to tell it what to do with the result, by setting the completion handler.

The delegate for the completion handler takes a value of type IAsyncOperation<StorageFile^>^ . Here we still don’t have a handle to the StorageFile – what we have is again an operation (completed or failed) from which we can get the StorageFile^ by calling a non-blocking method GetResults.

Finally, we must start the operation by calling the Start method.

In a non-trivial application multiple asynchronous operations are usually composed together. For example, writing a string into file requires chaining together four asynchronous operations, as in the following example:

 StorageFolder^ item = KnownFolders::PicturesLibrary;
 StorageFileRetrievalOperation^ createStorageFileOp = item->CreateFileAsync("myfile.txt");
  
 createStorageFileOp->Completed = ref new AsyncOperationCompletedHandler<StorageFile^>(
     [](IAsyncOperation<StorageFile^>^ asyncOp) 
     {
         StreamRetrievalOperation^ streamRetrievalOp = asyncOp->GetResults()->OpenAsync(FileAccessMode::ReadWrite);
  
         streamRetrievalOp->Completed = ref new AsyncOperationCompletedHandler<IRandomAccessStream^>(
             [](IAsyncOperation<IRandomAccessStream^>^ asyncOp) 
             {
                 IOutputStream^ stream = asyncOp->GetResults()->GetOutputStreamAt(0);
                 DataWriter^ bbrw = ref new DataWriter(stream);
  
                 bbrw->WriteString("Hello async!");
                 DataWriterStoreOperation^ writeBinaryStringOp = bbrw->StoreAsync();
  
                 writeBinaryStringOp->Completed = ref new AsyncOperationCompletedHandler<unsigned int>(
                 [stream](IAsyncOperation<unsigned int>^ asyncOp) 
                 {
                     int bytes = asyncOp->GetResults();
                     IAsyncOperation<bool>^ streamFlushOp = stream->FlushAsync();
  
                     streamFlushOp->Completed = ref new AsyncOperationCompletedHandler<bool>(
                     [](IAsyncOperation<bool>^ asyncOp) 
                     {
                         bool result = asyncOp->GetResults();
                     });
                     streamFlushOp->Start();
                 });
                 writeBinaryStringOp->Start();
             });
         streamRetrievalOp->Start();
     });
 createStorageFileOp->Start();

The example above assumes that none of the asynchronous operations can fail or be cancelled, which is an unrealistic expectation for a real-world application. When an error does occur, it can be handled in two ways. First, you can put the call to GetResults in a try block – when the operation fails, the call to GetResults will throw an exception. Second, you can check the status of the operation prior to calling GetResults:

 if( asyncOp->Status == AsyncStatus::Completed )
 {
     StreamRetrievalOperation^ streamRetrievalOp = 
         asyncOp->GetResults()->OpenAsync(FileAccessMode::ReadWrite);
     // Proceed...
 }
 else
 {
     // Handle error...
 }

In general, the error handling is needed for every asynchronous operation that can fail.

Windows Asynchrony with PPL

Using asynchronous operations is essential for building responsive and scalable applications, but it requires a new programming model and a new API that are frankly, harder to master than the traditional synchronous model.

We in the Concurrency Runtime Team pride ourselves in improving the experience of C++ developers writing concurrent code. As it turns out, the new model for asynchrony in Windows 8 maps pretty well to a set of concepts we introduced earlier this year, the PPL Tasks.

The concept of the asynchronous operation represents an “eventual” result and thus can be easily mapped to the concept of the task. Setting a completion event (the Completed property above) is conceptually similar to scheduling a continuation on the task.

Not only do we have a good mapping between the concepts, PPL Tasks significantly reduce the amount of boiler-plate code that needs to be written and compose well with each other.

Without further ado, let’s revisit the example of writing a string into a file and rewrite it using PPL Tasks:

 StorageFolder^ item = KnownFolders::PicturesLibrary;
  
 StorageFileRetrievalOperation^ createStorageFileOp = 
   item->CreateFileAsync("myfile.txt");
  
 task<StorageFile^> createFileTask(createStorageFileOp);
  
 createFileTask.then([](StorageFile^ storageFile) {
     return storageFile->OpenAsync(FileAccessMode::ReadWrite);
 }).then([](IRandomAccessStream^ rastream) -> task<bool> {
     IOutputStream^ ostream = rastream->GetOutputStreamAt(0);
     DataWriter^ bbrw = ref new DataWriter(ostream);
     bbrw->WriteString("Hello async!");
     task<unsigned int> writtenTask(bbrw->StoreAsync());
     return writtenTask.then([ostream](unsigned int bytesWritten) {
         return ostream->FlushAsync();
     });
 }).then([](bool flushed) {
 })

The constructor of the task accepts a parameter of type IAsyncOperation<T>^ , resulting in a task<T> . The continuation of that task – i.e. the lambda passed into the then method – executes when the operation used to instantiate the task completes. As you already know from our last post, the then method returns a task, allowing you to “chain up” multiple methods together.

For operations that can fail, you can create a continuation where the resulting value is wrapped into a task. To get a value out of the task, the user can call get – which will throw if the underlying asynchronous operation resulted in a failure:

 createFileTask.then([](task<StorageFile^> storageFileTask) -> 
     IAsyncOperation<IRandomAccessStream^>^ {
     try
     {
         auto storageFile = storageFileTask.get();
         return storageFile->OpenAsync(FileAccessMode::ReadWrite);
     }
     catch (std::exception& ex)
     {
         // Handle exception
     }
 })...

Fortunately, when you’re chaining together a list of continuations, not every continuation needs to handle errors. When an exception occurs in any of the tasks of the chain, the exception propagates “down” to the first error-handling continuation in the chain. This way only the last continuation of the chain needs to handle errors.

Hence we can re-write our PPL example above as follows:

 std::shared_ptr<IOutputStream^> outputStream = 
     std::make_shared<IOutputStream^>(nullptr); // avoid capturing locals
  
 StorageFolder^ item = Windows::Storage::KnownFolders::PicturesLibrary;
  
 auto op = item->CreateFileAsync("myfile.txt");
  
 task<StorageFile^> createFileTask(op);
  
 createFileTask.then([](StorageFile^ storageFile) {
     return storageFile->OpenAsync(FileAccessMode::ReadWrite);
 }).then([outputStream](IRandomAccessStream^ rastream) -> 
         IAsyncOperation<unsigned int>^ {
     *outputStream = rastream->GetOutputStreamAt(0);
     DataWriter^ bbrw = ref new DataWriter(*outputStream);
     bbrw->WriteString("Hello async!");
     return bbrw->StoreAsync();
 }).then([outputStream](task<unsigned int> writtenTask) {
     return (*outputStream)->FlushAsync();
 }).then([](task<bool> task_flushed) {
     // This is the error-handling continuation
     try
     {
         bool flushed = task_flushed.get();
         // Report success
     }
     catch (std::exception& ex)
     {
         // Report failure
     }
 });

We think that PPL will make using Windows 8 asynchronous operations significantly simpler, but we want to hear your feedback. Do you find it easy to use? Would you use Windows 8 asynchronous operations with or without PPL?

We’ve added a few more advanced samples to our sample pack – go ahead and download it, experiment with the samples and let us know what you think!

Artur Laksberg
Concurrency Runtime Team

Comments

  • Anonymous
    October 03, 2011
    Really powerful stuff, I love it. Will this ship with the next version of Visual Studio?

  • Anonymous
    October 03, 2011
    Thanks Matt! Yes, we want to make it part of the product. But since we're not done with it yet, I will not promise when that happens -- stay tuned for news on this blog! Artur

  • Anonymous
    October 11, 2011
    Does ConcRT still use the User Mode Scheduler that was introduced in Windows 7? How does this interact with the WinRT threading system, if anything?

  • Anonymous
    October 12, 2011
    Hi Jack, Thanks for your note. We are retiring the UMS scheduler for the upcoming versions. The concurrency runtime will not take advantage of the UMS threads. Going forward, we will internally map requests for UMSThreadDefault to ThreadScheduler. Are you using UMSThreadDefault today? If so we’d love to hear from you to understand how and where you use it. Also, what level of details would you like to know about the interaction with WinRT? Thanks! Rahul

  • Anonymous
    February 02, 2012
    The comment has been removed