Thursday, September 8, 2016

C++ Background Threads on Windows Universal

Solving a SAT problem can take a long time. For a console application this is not too horrific but in a Windows forms application of any kind, the user interface can and will timeout. The correct way to solve this problem is by putting the processing of the SAT problem on a background thread. The user interface spawns a thread, puts the processing on it, and the thread in turns notifies the user interface as to its progress.

Now, in classic Windows Forms C++, using the SDK, a simple mechanism for using a background thread is create a worker thread using either CreateThread or, to use the threadpool APIs. Now, the Windows SDK is seemingly forgivable when invoking Windows objects on background threads. Many windows user interface calls use SendMessage behind the scenes and SendMessage performs a context switch on your behalf. While this can work, the most effective way to manage thread communications to a user interface is to PostMessage. We allocate a message on the background thread, PostMessage to our recipient Windows, process that message, and then free it, on the Windows thread. Windows has worked this way since Windows 95, but it was really Windows NT finally Windows 2000 where Microsoft put this model altogether.

So much is different in Windows Universal, yet, this basic model remains in the Windows Universal API In Windows 10, using the Windows runtime, this process is similar. The names, however, have been changed to protect the innocent. The good news is, we can actually use a lot of the SDK thread primitives in C++ Windows Store applications. I was able to do both CreateThread and use a CreateMutex and WaitForSingleObject seemingly successfully. However, the matter of PostMessage is quite different. In any case, a good pattern is to use ThreadPool::RunAsync to queue a job to the background thread. Then, the body of the thread uses Dispatcher->RunAsync to essentially post a message back to the Windows thread.

Now, for OverSATX, the solver objects must be accessed in both the foreground thread and the worker thread. The first answer, of course, is to use locks to manage that. IT turns out that our trusty CreateMutex WaitForSingleObject actually works, but locks are messy and increase the risk of bugs, not to mention, undermine performance. Fortunately in OverSATX, it turns out that this is not necessary. Instead, in OverSATX, since I always have the solution state, I let the background thread run for a second, and post the results of its work to the foreground thread. The foreground thread then queues another job to the background thread. This granularity works rather well. Even with the feeble conflict resolution engine of OverSATX, I can for some problems perform between 100-1000 trials on the SAT problem in that second when doing a release build. A future change to the conflict engine should improve that performance dramatically.

How to Get the Current Time in C++ using the Windows Universal Runtime

You can obtain the current time and as follows:

  Windows::Globalization::Calendar^ endTime = ref new Windows::Globalization::Calendar();
  endTime->SetToNow();

Managing running something for a set period of time in C++ in Windows Universal

You can get the current time, add a few seconds to it, then, compare that to the present time in your processing loop


  Windows::Globalization::Calendar^ currentTime = ref new Windows::Globalization::Calendar();
  Windows::Globalization::Calendar^ endTime = ref new Windows::Globalization::Calendar();
  endTime->SetToNow();
  endTime->AddSeconds(1);
  Windows::Foundation::DateTime endDateTime = endTime->GetDateTime();
  sat::sat_solver_strategy strategy;
  bool finished = false;
  while (!finished && IsRunning)
  {
   if (sat_solver)
   {
    finished = sat_solver->step(strategy);
    currentTime->SetToNow();
    if (currentTime->CompareDateTime(endDateTime) > 0 || finished)
    {

Background Thread in C++ Windows Universal

Notice that, in the below, when we are inside the dispatcher, calling the U/I thread back, we're actually a function call that lives on the foreground thread. That is why setting UI elements works there. If we were to alter members of the SAT_Solver at that point, we could actually be introducing a race condition. We live seemingly risky, but safely, handing off the work from one thread to another, as if two ice skaters tossing a ball between them.

void OverSatX::MainPage::RunBackground()
{
 IsRunning = true;
 IsStopped = false;
 btnSATStep->IsEnabled = false;
 btnSATCalc->IsEnabled = false;
 btnCnfLoad->IsEnabled = false;
 btnSATStop->IsEnabled = true;
 SetStatus(L"RUNNING");

 ThreadPool::RunAsync(ref new WorkItemHandler([this](IAsyncAction ^async)
 {
  Windows::Globalization::Calendar^ currentTime = ref new Windows::Globalization::Calendar();
  Windows::Globalization::Calendar^ endTime = ref new Windows::Globalization::Calendar();
  endTime->SetToNow();
  endTime->AddSeconds(1);
  Windows::Foundation::DateTime endDateTime = endTime->GetDateTime();

  sat::sat_solver_strategy strategy;
  bool finished = false;
  while (!finished && IsRunning)
  {
   if (sat_solver)
   {
    finished = sat_solver->step(strategy);
    currentTime->SetToNow();
    if (currentTime->CompareDateTime(endDateTime) > 0 || finished)
    {
     IsRunning = false;
     IsStopped = true;
     Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(
      Windows::UI::Core::CoreDispatcherPriority::High,
      ref new Windows::UI::Core::DispatchedHandler([this, finished]()
     {
      DrawEverything(finished);
      if (!finished) 
      {
       RunBackground();
      }
     }));
     return;
    }
   }
  }

  if (!finished)
  {
   Windows::ApplicationModel::Core::CoreApplication::MainView->CoreWindow->Dispatcher->RunAsync(
    Windows::UI::Core::CoreDispatcherPriority::High,
    ref new Windows::UI::Core::DispatchedHandler([this, finished]()
   {
    // nasty side effects driven business... but that's why this app is a learning experience.
    btnSATStep->IsEnabled = true;
    btnSATCalc->IsEnabled = true;
    btnCnfLoad->IsEnabled = true;
    btnSATStop->IsEnabled = false;
    SetStatus(L"STOPPED");
   }));
  }

  IsStopped = true;
 }));
}

No comments:

Post a Comment