Книга: Windows API Tutorials

Using Threads

Using Threads

Multitasking is one of the most difficult aspects of programming. It makes it even more important to provide a simple set of abstractions and to encapsulate it in a nice object-oriented shell. In the OO world, the natural counterpart of the thread (which is a strictly procedural abstraction) is the Active Object. An active object owns a captive thread that performs certain tasks asynchronously. This thread has access to all the internal (private) data and methods of the object. The public interface of the Active Object is available to external agents (e.g., to the main thread, or to the Windows thread carrying messages) so that they, too, can manipulate the state of the object and that of the captive thread, albeit in a very controlled and restricted manner.

An Active Object is built upon a framework called ActiveObject. The implementor of the derived class is supposed to provide the implementation for the pure virtual methods InitThread, Run and Flush (as well as write the destructor).

class ActiveObject {
public:
  ActiveObject ();
  virtual ~ActiveObject () {}
  void Kill ();
protected:
 virtual void InitThread () = 0;
 virtual void Run () = 0;
 virtual void FlushThread () = 0;
 static DWORD WINAPI ThreadEntry (void *pArg);
 int _isDying;
 Thread _thread;
};

The constructor of the ActiveObject initializes the captive thread by passing it a pointer to a function to be executed and a pointer to "this." We have to disable the warning about using "this" before it is fully constructed. We know that it won't be used too early, because the thread is created in the inactive state. The constructor of the derived class is supposed to call _thread.Resume() in order to activate it.

// The constructor of the derived class
// should call
//  _thread.Resume ();
// at the end of construction
ActiveObject::ActiveObject () : _isDying (0),
#pragma warning(disable: 4355) // 'this' used before initialized
  _thread (ThreadEntry, this)
#pragma warning(default: 4355)
{
}

The method Kill calls the virtual method FlushThread — it is supposed to release the thread from any wait state and let it run ahead to check the _isDying flag.

void ActiveObject::Kill () {
 _isDying++;
 FlushThread ();
 // Let's make sure it's gone
 _thread.WaitForDeath ();
}

We also have a framework for the ThreadEntry function (it's a static method of ActiveObject, so we can specify the calling convention required by the API). This function is executed by the captive thread. The argument it gets from the system is the one we passed to the constructor of the thread object — the "this" pointer of the Active Object. The API expects a void pointer, so we have to do an explicit cast to the ActiveObject pointer. Once we get hold of the Active Object, we call its pure virtual method InitThread, to make all the implementation specific preparations, and then call the main worker method, Run. The implementation of Run is left to the client of the framework.

DWORD WINAPI ActiveObject::ThreadEntry (void* pArg){   
 ActiveObject * pActive = (ActiveObject *) pArg;
 pActive->InitThread ();
 pActive->Run ();
 return 0;
}

The Thread object is a thin encapsulation of the API. Notice the flag CREATE_SUSPENDED which assures that the thread doesn't start executing before we are done with the construction of the ActiveObject.

class Thread{
public:
  Thread ( DWORD (WINAPI * pFun) (void* arg), void* pArg) {
   _handle = CreateThread (
    0, // Security attributes
    0, // Stack size
    pFun, pArg, CREATE_SUSPENDED, &_tid);
  }
  ~Thread () { CloseHandle (_handle); }
  void Resume () { ResumeThread (_handle); }
  void WaitForDeath () {
   WaitForSingleObject (_handle, 2000);
  }
private:
 HANDLE _handle;
 DWORD  _tid;     // thread id
};

Synchronization is what really makes multitasking so hard. Let's start with mutual exclusion. Class Mutex is a thin encapsulation of the API. You embed Mutexes in your Active Object and then use them through Locks. A Lock is a clever object that you construct on the stack and for the duration of its lifetime your object is protected from any other threads. Class Lock is one of the applications of the Resource Management methodology. You have to put Locks inside all the methods of your Active Object that access data shared with the captive thread.

class Mutex {
 friend class Lock;
public:
 Mutex () { InitializeCriticalSection (& _critSection); }
 ~Mutex () { DeleteCriticalSection (& _critSection); }
private:
 void Acquire () {
  EnterCriticalSection (& _critSection);
 }
 void Release () {
  LeaveCriticalSection (& _critSection);
 }
 CRITICAL_SECTION _critSection;
};
class Lock {
public:
 // Acquire the state of the semaphore
 Lock ( Mutex & mutex ) : _mutex(mutex) {
  _mutex.Acquire();
 }
 // Release the state of the semaphore
 ~Lock () {
  _mutex.Release();
 }
private:
 Mutex & _mutex;
};

An Event is a signalling device that threads use to communicate with each other. You embed an Event in your active object. Then you make the captive thread wait on it until some other thread releases it. Remember however that if your captive thread waits on a event it can't be terminated. That's why you should call Release from the Flush method.

class Event {
public:
 Event () {
  // start in non-signaled state (red light)
  // auto reset after every Wait
  _handle = CreateEvent (0, FALSE, FALSE, 0);
 }
 ~Event () {
  CloseHandle (_handle);
 }
 // put into signaled state
 void Release () { SetEvent (_handle); }
 void Wait () {
  // Wait until event is in signaled (green) state
  WaitForSingleObject (_handle, INFINITE);
 }
 operator HANDLE () { return _handle; }
private:
 HANDLE _handle;
};

To see how these classes can be put to use, I suggest a little side trip. You can jump to the page that explains how the Frequency Analyzer uses the activeobject class to update the display asynchronously. Or, you can study a somehow simpler example of a Folder Watcher that waits quietly watching a folder and wakes up only when a change happens.

I wish I could say programming with threads was simple. It is, however, simpler if you use the right kind of primitives. The primitives I advertised here are ActiveObject, Thread, Mutex, Lock and Event. Some of them are actually available in MFC. For instance, they have a Lock object (or is it CLock?) whose destructor releases the critical section (theirs is slightly less convenient because of the "two-step" construction — you have to create it and then take it in two separate steps — as if you'd want to create it and then not take it). Other than that, MFC only offers some thin veneer over the API, nothing exciting.

You might also recognize some of the mechanisms I presented here in the Java programming language. Of course, when you have the liberty of designing multitasking into the language, you can afford to be elegant. Their version of ActiveObject is called Runnable and it has the run method. Every Java object potentially has a built-in mutex and all you have to do to take a lock is to declare a method (or a scope) synchronized. Similarly, the events are implemented with the wait and notify calls inside any synchronized method. So, if you know how to program in Java, you know how to program in C++ (as if there were people fluent in Java, but ignorant of C++).

Оглавление книги


Генерация: 0.789. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз