2012-01-31

ThreadClass::WAITSAFE

A New Year's Resolution, if you can call it that, is to blog more in the moment, as I'm figuring stuff out. I think I just figured out how the ThreadClass::WAITSAFE option works, so here goes...

ThreadClass::WAITSAFE is an enum value under ThreadClass::WaitOptions. It can be passed to several of the different Wait() methods related to the threading classes. The gloss on it says "Exits early if thread stops running," but there aren't any examples that show it in action and it seems to be the default parameter for some of the Wait() methods, but not for all of them. Specifically, it's not the default parameter for either of the ThreadClass::WaitClass methods.

To test it, we're going to have three threads. One is just going to spin its wheels—it's the thread we'd have doing real work, so we'll call it the "doer." Another thread will launch the doer and wait for it to finish, which we'll call the "waiter." This seems kind of silly, except that in a real application, the waiter could have a pool of doer threads, and could be coordinating their work and that may justify a separate thread of its own (especially if it's happening while you have a dialog up). Finally, we'll have our main thread that creates and launches the waiter, sleeps for a bit, then tells it to cancel. The waiter in turn will cancel the doer thread, and exit once the doer has exited. Here's the code:



class Doer: ThreadClass {
  Doer(): ThreadClass() {}

  virtual void Run() {
    while (IsRunning()) {
      Console.WriteLine("Doer sleeping...");
      SystemClass::Sleep(1000);
    }
    Console.WriteLine("Doer exiting.");
  }
}

class Waiter: ThreadClass {
  Waiter(): ThreadClass() {}

  virtual void Run() {
    Doer d();
    ThreadClass::WaitClass w();
    Console.WriteLine("Starting doer...");
    d.Start();
    w.AddObject(d);
    Console.WriteLine("Waiting on doer...");
    bool waitRet = w.WaitAll();
    Console.WriteLine("Woke up waiter");
    if (!IsRunning()) {
      Console.WriteLine("Waiter was cancelled. Stopping doer...");
      d.StopRunning();
      d.Wait();
    }
    Console.WriteLine("Waiter exiting, waitRet = " + waitRet);
  }
}

class MainClass {
  void Main() {
    SystemClass::ClearConsole();
    Waiter t();
    Console.WriteLine("Starting waiter...");
    t.Start();
    Console.WriteLine("Sleeping for 10 seconds");
    SystemClass::Sleep(10000);
    Console.WriteLine("Cancelling waiter...");
    t.StopRunning();
    Console.WriteLine("Waiting for waiter to quit...");
    t.Wait();
    Console.WriteLine("Waiter returned successfully. Done.");
  }
}

This code produces the following output:


Starting waiter...
Sleeping for 10 seconds
Starting doer...
Waiting on doer...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Cancelling waiter...
Waiting for waiter to quit...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
...


The problem is that this script never terminates successfully. The Waiter class uses WaitAll() to wait for the Doer thread to finish, but the Doer thread is just hanging out, enjoying its infinite loop until someone calls StopRunning() on it... which never happens. This is where ThreadClass::WAITSAFE comes in. Here's some modified code:


class Doer: ThreadClass {
  Doer(): ThreadClass() {}

  virtual void Run() {
    while (IsRunning()) {
      Console.WriteLine("Doer sleeping...");
      SystemClass::Sleep(1000);
    }
    Console.WriteLine("Doer exiting.");
  }
}

class Waiter: ThreadClass {
  Waiter(): ThreadClass() {}

  virtual void Run() {
    Doer d();
    ThreadClass::WaitClass w();
    Console.WriteLine("Starting doer...");
    d.Start();
    w.AddObject(d);
    Console.WriteLine("Waiting on doer...");
    bool waitRet = w.WaitAll(ThreadClass::INFINITE, ThreadClass::WAITSAFE);
    Console.WriteLine("Woke up waiter");
    if (!IsRunning()) {
      Console.WriteLine("Waiter was cancelled. Stopping doer...");
      d.StopRunning();
      d.Wait();
    }
    Console.WriteLine("Waiter exiting, waitRet = " + waitRet);
  }
}

class MainClass {
  void Main() {
    SystemClass::ClearConsole();
    Waiter t();
    Console.WriteLine("Starting waiter...");
    t.Start();
    Console.WriteLine("Sleeping for 10 seconds");
    SystemClass::Sleep(10000);
    Console.WriteLine("Cancelling waiter...");
    t.StopRunning();
    Console.WriteLine("Waiting for waiter to quit...");
    t.Wait();
    Console.WriteLine("Waiter returned successfully. Done.");
  }
}


This time, we pass ThreadClass::INFINITE (the default) and ThreadClass::WAITSAFE (not the default) as parameters to the WaitAll() method in the Waiter. This is what we get for output:


Starting waiter...
Sleeping for 10 seconds
Starting doer...
Waiting on doer...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Cancelling waiter...
Waiting for waiter to quit...
Doer sleeping...
Woke up waiter
Waiter was cancelled. Stopping doer...
Doer exiting.
Waiter exiting, waitRet = 0
Waiter returned successfully. Done.


Huzzah! Calling StopRunning() in the main thread on the waiter causes the waiter to wake up from WakeAll(), now that we're using ThreadClass::WAITSAFE. Once the Waiter wakes up, it checks IsRunning() to check whether it's been given the signal to quit, via StopRunning(), and then does likewise with the Doer thread. The Doer exits gracefully, then the Waiter exits gracefully, and then the whole script exists. Quite nice.

Notice that we've output the return value from WaitAll(), and it seems to have been false in this case. The question this raises is, would it return true if Doer exited before Waiter was cancelled with StopRunning()? Here's some modified code where the Doer exits after about 5 seconds, while the main thread still waits about 10 seconds to tell the Waiter to stop.



class Doer: ThreadClass {
  Doer(): ThreadClass() {}

  virtual void Run() {
    uint i;
    while (IsRunning() && i++ < 5) {
      Console.WriteLine("Doer sleeping...");
      SystemClass::Sleep(1000);
    }
    Console.WriteLine("Doer exiting.");
  }
}

class Waiter: ThreadClass {
  Waiter(): ThreadClass() {}

  virtual void Run() {
    Doer d();
    ThreadClass::WaitClass w();
    Console.WriteLine("Starting doer...");
    d.Start();
    w.AddObject(d);
    Console.WriteLine("Waiting on doer...");
    bool waitRet = w.WaitAll(ThreadClass::INFINITE, ThreadClass::WAITSAFE);
    Console.WriteLine("Woke up waiter");
    if (!IsRunning()) {
      Console.WriteLine("Waiter was cancelled. Stopping doer...");
      d.StopRunning();
      d.Wait();
    }
    Console.WriteLine("Waiter exiting, waitRet = " + waitRet);
  }
}

class MainClass {
  void Main() {
    SystemClass::ClearConsole();
    Waiter t();
    Console.WriteLine("Starting waiter...");
    t.Start();
    Console.WriteLine("Sleeping for 10 seconds");
    SystemClass::Sleep(10000);
    Console.WriteLine("Cancelling waiter...");
    t.StopRunning();
    Console.WriteLine("Waiting for waiter to quit...");
    t.Wait();
    Console.WriteLine("Waiter returned successfully. Done.");
  }
}


And here's the output:


Starting waiter...
Sleeping for 10 seconds
Starting doer...
Waiting on doer...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer sleeping...
Doer exiting.
Woke up waiter
Waiter exiting, waitRet = 1
Cancelling waiter...
Waiting for waiter to quit...
Waiter returned successfully. Done.


So, as you can see, WaitAll() still returns when the Doer thread exits, and this time it returns true. Waiter then exits from its Run() function before the main thread has the chance to cancel it. The upshot is that we can use the return value from WaitAll() to tell whether the thread waiting has had StopRunning() called on it (return value is false) or whether simply the operations it's waiting on have completed (return value is true).

Oh frabjous day, everything works!