Share via


How to: Use Oversubscription to Offset Latency

 

The new home for Visual Studio documentation is Visual Studio 2017 Documentation on docs.microsoft.com.

Oversubscription can improve the overall efficiency of some applications that contain tasks that have a high amount of latency. This topic illustrates how to use oversubscription to offset the latency that is caused by reading data from a network connection.

Example

This example uses the Asynchronous Agents Library to download files from HTTP servers. The http_reader class derives from concurrency::agent and uses message passing to asynchronously read which URL names to download.

The http_reader class uses the concurrency::task_group class to concurrently read each file. Each task calls the concurrency::Context::Oversubscribe method with the _BeginOversubscription parameter set to true to enable oversubscription in the current context. Each task then uses the Microsoft Foundation Classes (MFC) CInternetSession and CHttpFile classes to download the file. Finally, each task calls Context::Oversubscribe with the _BeginOversubscription parameter set to false to disable oversubscription.

When oversubscription is enabled, the runtime creates one additional thread in which to run tasks. Each of these threads can also oversubscribe the current context and thereby create additional threads. The http_reader class uses a concurrency::unbounded_buffer object to limit the number of threads that the application uses. The agent initializes the buffer with a fixed number of token values. For each download operation, the agent reads a token value from the buffer before the operation starts and then writes that value back to the buffer after the operation finishes. When the buffer is empty, the agent waits for one of the download operations to write a value back to the buffer.

The following example limits the number of simultaneous tasks to two times the number of available hardware threads. This value is a good starting point to use when you experiment with oversubscription. You can use a value that fits a particular processing environment or dynamically change this value to respond to the actual workload.

// download-oversubscription.cpp
// compile with: /EHsc /MD /D "_AFXDLL"
#define _WIN32_WINNT 0x0501
#include <afxinet.h>
#include <concrtrm.h>
#include <agents.h>
#include <ppl.h>
#include <sstream>
#include <iostream>
#include <array>

using namespace concurrency;
using namespace std;

// Calls the provided work function and returns the number of milliseconds 
// that it takes to call that function.
template <class Function>
__int64 time_call(Function&& f)
{
   __int64 begin = GetTickCount();
   f();
   return GetTickCount() - begin;
}

// Downloads the file at the given URL.
CString GetHttpFile(CInternetSession& session, const CString& strUrl);

// Reads files from HTTP servers.
class http_reader : public agent
{
public:
   explicit http_reader(CInternetSession& session,      
      ISource<string>& source,
      unsigned int& total_bytes,
      unsigned int max_concurrent_reads)
      : _session(session)
      , _source(source)
      , _total_bytes(total_bytes)
   {
      // Add one token to the available tasks buffer for each 
      // possible concurrent read operation. The value of each token 
      // is not important, but can be useful for debugging.
      for (unsigned int i = 0; i < max_concurrent_reads; ++i)
         send(_available_tasks, i);
   }

   // Signals to the agent that there are no more items to download.
   static const string input_sentinel;
 
protected:
   void run()
   {
      // A task group. Each task in the group downloads one file.
      task_group tasks;

      // Holds the total number of bytes downloaded.
      combinable<unsigned int> total_bytes;

      // Read from the source buffer until the application 
      // sends the sentinel value.
      string url;
      while ((url = receive(_source)) != input_sentinel)
      {
         // Wait for a task to release an available slot.
         unsigned int token = receive(_available_tasks);

         // Create a task to download the file.
         tasks.run([&, token, url] {

            // Print a message.
            wstringstream ss;
            ss << L"Downloading " << url.c_str() << L"..." << endl;
            wcout << ss.str();

            // Download the file.
            string content = download(url);

            // Update the total number of bytes downloaded.
            total_bytes.local() += content.size();

            // Release the slot for another task.
            send(_available_tasks, token);
         });
      }

      // Wait for all tasks to finish.
      tasks.wait();
      
      // Compute the total number of bytes download on all threads.
      _total_bytes = total_bytes.combine(plus<unsigned int>());

      // Set the status of the agent to agent_done.
      done();
   }

   // Downloads the file at the given URL.
   string download(const string& url)
   {
      // Enable oversubscription.
      Context::Oversubscribe(true);

      // Download the file.
      string content = GetHttpFile(_session, url.c_str());
      
      // Disable oversubscription.
      Context::Oversubscribe(false);

      return content;
   }

private:
   // Manages the network connection.
   CInternetSession& _session;
   // A message buffer that holds the URL names to download.
   ISource<string>& _source;
   // The total number of bytes downloaded
   unsigned int& _total_bytes;
   // Limits the agent to a given number of simultaneous tasks.
   unbounded_buffer<unsigned int> _available_tasks;
};
const string http_reader::input_sentinel("");

int wmain()
{
   // Create an array of URL names to download.
   // A real-world application might read the names from user input.
   array<string, 21> urls = {
      "https://www.adatum.com/",
      "https://www.adventure-works.com/", 
      "https://www.alpineskihouse.com/",
      "https://www.cpandl.com/", 
      "https://www.cohovineyard.com/",
      "https://www.cohowinery.com/",
      "https://www.cohovineyardandwinery.com/", 
      "https://www.contoso.com/",
      "https://www.consolidatedmessenger.com/",
      "https://www.fabrikam.com/", 
      "https://www.fourthcoffee.com/",
      "https://www.graphicdesigninstitute.com/",
      "https://www.humongousinsurance.com/",
      "https://www.litwareinc.com/",
      "https://www.lucernepublishing.com/",
      "https://www.margiestravel.com/",
      "https://www.northwindtraders.com/",
      "https://www.proseware.com/", 
      "https://www.fineartschool.net",
      "https://www.tailspintoys.com/",
      http_reader::input_sentinel,
   };
      
   // Manages the network connection.
   CInternetSession session("Microsoft Internet Browser");

   // A message buffer that enables the application to send URL names to the 
   // agent.
   unbounded_buffer<string> source_urls;

   // The total number of bytes that the agent has downloaded.
   unsigned int total_bytes = 0u;

   // Create an http_reader object that can oversubscribe each processor by one.
   http_reader reader(session, source_urls, total_bytes, 2*GetProcessorCount());

   // Compute the amount of time that it takes for the agent to download all files.
   __int64 elapsed = time_call([&] {
      
      // Start the agent.
      reader.start();
      
      // Use the message buffer to send each URL name to the agent.
      for_each(begin(urls), end(urls), [&](const string& url) {
         send(source_urls, url);
      });

      // Wait for the agent to finish downloading.
      agent::wait(&reader);      
   });

   // Print the results.
   wcout << L"Downloaded " << total_bytes
         << L" bytes in " << elapsed << " ms." << endl;
}

// Downloads the file at the given URL and returns the size of that file.
CString GetHttpFile(CInternetSession& session, const CString& strUrl)
{
   CString strResult;

   // Reads data from an HTTP server.
   CHttpFile* pHttpFile = NULL;

   try
   {
      // Open URL.
      pHttpFile = (CHttpFile*)session.OpenURL(strUrl, 1, 
         INTERNET_FLAG_TRANSFER_ASCII | 
         INTERNET_FLAG_RELOAD | INTERNET_FLAG_DONT_CACHE);

      // Read the file.
      if(pHttpFile != NULL)
      {           
         UINT uiBytesRead;
         do
         {
            char chBuffer[10000];
            uiBytesRead = pHttpFile->Read(chBuffer, sizeof(chBuffer));
            strResult += chBuffer;
         }
         while (uiBytesRead > 0);
      }
    }
   catch (CInternetException)
   {
      // TODO: Handle exception
   }

   // Clean up and return.
   delete pHttpFile;

   return strResult;
}

This example produces the following output on a computer that has four processors:

Downloading https://www.adatum.com/...  
Downloading https://www.adventure-works.com/...  
Downloading https://www.alpineskihouse.com/...  
Downloading https://www.cpandl.com/...  
Downloading https://www.cohovineyard.com/...  
Downloading https://www.cohowinery.com/...  
Downloading https://www.cohovineyardandwinery.com/...  
Downloading https://www.contoso.com/...  
Downloading https://www.consolidatedmessenger.com/...  
Downloading https://www.fabrikam.com/...  
Downloading https://www.fourthcoffee.com/...  
Downloading https://www.graphicdesigninstitute.com/...  
Downloading https://www.humongousinsurance.com/...  
Downloading https://www.litwareinc.com/...  
Downloading https://www.lucernepublishing.com/...  
Downloading https://www.margiestravel.com/...  
Downloading https://www.northwindtraders.com/...  
Downloading https://www.proseware.com/...  
Downloading https://www.fineartschool.net...  
Downloading https://www.tailspintoys.com/...  
Downloaded 1801040 bytes in 3276 ms.  

The example can run faster when oversubscription is enabled because additional tasks run while other tasks wait for a latent operation to finish.

Compiling the Code

Copy the example code and paste it in a Visual Studio project, or paste it in a file that is named download-oversubscription.cpp and then run one of the following commands in a Visual Studio Command Prompt window.

cl.exe /EHsc /MD /D "_AFXDLL" download-oversubscription.cpp

cl.exe /EHsc /MT download-oversubscription.cpp

Robust Programming

Always disable oversubscription after you no longer require it. Consider a function that does not handle an exception that is thrown by another function. If you do not disable oversubscription before the function returns, any additional parallel work will also oversubscribe the current context.

You can use the Resource Acquisition Is Initialization (RAII) pattern to limit oversubscription to a given scope. Under the RAII pattern, a data structure is allocated on the stack. That data structure initializes or acquires a resource when it is created and destroys or releases that resource when the data structure is destroyed. The RAII pattern guarantees that the destructor is called before the enclosing scope exits. Therefore, the resource is correctly managed when an exception is thrown or when a function contains multiple return statements.

The following example defines a structure that is named scoped_blocking_signal. The constructor of the scoped_blocking_signal structure enables oversubscription and the destructor disables oversubscription.

struct scoped_blocking_signal
{
    scoped_blocking_signal()
    {
        concurrency::Context::Oversubscribe(true);  
    }
    ~scoped_blocking_signal()
    {
        concurrency::Context::Oversubscribe(false);
    }
};

The following example modifies the body of the download method to use RAII to ensure that oversubscription is disabled before the function returns. This technique ensures that the download method is exception-safe.

// Downloads the file at the given URL.
string download(const string& url)
{
   scoped_blocking_signal signal;

   // Download the file.
   return string(GetHttpFile(_session, url.c_str()));
}

See Also

Contexts
Context::Oversubscribe Method