============================================================================== C-Scene Issue #3 Thread Loop Classes for Server Apps Chad Loder ==============================================================================Note: All my examples will use the Win32 API, but it should not be too hard to generalize to POSIX. I assume some familiarity with the Win32 API, especially its multithreading functions. If you are not familiar with these functions, I suggest you read Richter's Advanced Windows books.
I also assume that you are familiar with C++ objects (including virtual and pure virtual functions, constructors and destructors, inheritance, etc.). I have also, for pedagogical reasons, not included bug-catching devices like asserts and error condition handling. In your code, you should always either handle a condition gracefully, or detect it and notify the user in debug mode.
Finally, some Windows programmers may notice that I have mixed Win32 API functions (like CreateThread) with C Run-Time Library functions - this is also for pedagogical purposes. In real life, I wouldn't.
A common component of a multithreaded server is a threaded-off routine that loops until some event occurs. In this article, I will demonstrate how you can encapsulate this type of routine in what I call a "thread loop class".
A typical concurrent network server might have one routine for each of the following tasks, each running on a different thread:
Of course, this list will vary; some servers treat the console as merely another client connection. Servers with a high level of concurrency might not create a separate thread for each client connection (for performance reasons), and there are usually many other tasks in a given server with this kind of behavior.
All of these tasks will keep looping for input until some event (either external or based on that input) tells them that it's time to stop. Then they will need to shut themselves down gracefully (warning clients, giving clients a chance to disconnect, flushing data to a cache, freeing resources, etc.).
How do we encapsulate this behavior in a class? Well, each thread has many things in common, but each thread also has at least one very different function that loops. This suggests that we create an abstract base class and derive from it for each type of loop routine we want to encapsulate. This loop routine is the one where all the action occurs, the threaded-off one; we will call it the action proc; it will not be implemented in the base class because there is no meaningful default action for a thread loop.
There is one slight complication. The action proc in a derived class will need to have static linkage in order for us to thread it off, because we have to pass its address to the CreateThread() API function. But if we implement the action proc as static, then we will not be able to call non-static functions or access non-static data for the class from the proc, which would in fact make the class useless.
We can get around this difficulty with a little sleight of hand. It will make our class a little uglier, but once implemented, we won't have to think about it any more. We will make the action proc static, but we will pass it a pointer which corresponds to the actual this pointer for the instance of the class (via which we can access non-static data and functions). In the base class, we will implement the Initialize() member, which will call the CreateThread() API function, passing it the address of the action proc, which is returned by a non-static pure virtual member called GetActionProc, which has to be implemented in the derived class. It sounds complex, but it's easy when you look at the code:
#include <windows.h> // abstract base class class ThreadLoop { public: // constructor ThreadLoop() { m_hQuit = CreateEvent(NULL, TRUE, FALSE, "QuitEvent"); m_hCleanup = CreateEvent(NULL, TRUE, FALSE, "SuccessQuitEvent"); }; // default destructor - clean up and free memory (implemented below) virtual ~ThreadLoop(); // creates the action thread in a suspended state; call Start() to activate virtual void Initialize() { m_hThread = CreateThread (NULL, 0, GetActionProc(), (LPVOID)this, CREATE_SUSPENDED, &m_dwThreadID); }; // activates the action thread; must call Initialize() first. virtual void Start() { ResumeThread(m_hThread); }; // stops the action thread permanently by triggering quit event virtual void Stop() { SetEvent(m_hQuit); }; // suspends the action thread until Start() is called again virtual void Sleep() { SuspendThread(m_hThread); }; virtual void QuitOK() { SetEvent(m_hCleanup); }; // implement in derived class - returns addr of the action proc virtual LPTHREAD_START_ROUTINE GetActionProc(void) const = 0; protected: HANDLE m_hThread; // handle to the action proc's thread DWORD m_dwThreadID; // the ID of the action proc's thread HANDLE m_hQuit; // manual event, set when Stop() is called HANDLE m_hCleanup; // set when loop has cleaned up after itself }; ThreadLoop::~ThreadLoop() { DWORD dwThreadStatus; /* if m_hCleanup has not been set, we assume that the server has not been notified of a shutdown, so try to notify it ourselves and see what happens */ if (WAIT_TIMEOUT == WaitForSingleObject(m_hCleanup, 0)) { Stop(); // try graceful shutdown by issuing command // after calling stop function, wait two seconds for cleanup if (WAIT_TIMEOUT == WaitForSingleObject(m_hCleanup, 2000)) { GetExitCodeThread(m_hThread, &dwThreadStatus); // if the thread is still active, we will murder it if (STILL_ACTIVE == dwThreadStatus) { TerminateThread(m_hThread, 1); } } } CloseHandle(m_hQuit); CloseHandle(m_hCleanup); }
Notice also that most of the member functions in the base class are virtual ( including the destructor). I have done this for two reasons: First, we cannot foresee all the needs of derived classes - they might need to override some of the default behavior; Second, if we are using pointers, we want the appropriate function to be called based on the type of object pointed to, not on the type of pointer we are using. For example, if we do something like this:
ThreadLoop* theLoop = new DerivedLoop; // ...execute some code using this loop delete theLoop;
Now we will derive a class from ThreadLoop. This class will be the console portion of a generic network server. It will process input from the console.
#include <iostream.h> #include <conio.h> class Console : public ThreadLoop { public: Console() : ThreadLoop() { }; ~Console() { CloseHandle(m_hInput); }; // the action proc, implemented below static DWORD WINAPI KeyboardLoop(void* const p); // returns the address of the action proc LPTHREAD_START_ROUTINE GetActionProc(void) const { return KeyboardLoop; } private: HANDLE m_hInput; // stdin char m_szInput[256];// user input }; // implemented as DWORD WINAPI: correct calling convention for CreateThread() DWORD WINAPI Console::KeyboardLoop(void* const p) { // p is the this pointer Console *thisConsole = (Console *)p; // get STDIN, so we can sleep on it while waiting for input thisConsole->m_hInput = GetStdHandle(STD_INPUT_HANDLE); // little array used for WaitForMultipleObjects() HANDLE phObjects[] = { thisConsole->m_hQuit, thisConsole->m_hInput }; bool bContinue = TRUE; DWORD dwWaitReturn; while (bContinue) { cout << "[-Console ready-] "; cout.flush(); // wait either for keyboard input or for a Stop() event dwWaitReturn = WaitForMultipleObjects(2, phObjects, FALSE, INFINITE); switch (dwWaitReturn - WAIT_OBJECT_0) { case 0: // Stop() event, so exit the loop bContinue = FALSE; continue; case 1: // input from the console, read it cin.getline( m_szInput, 255, '\n' ); // you will obviously want to parse this in real life if (0 == strcmp(m_szInput, "quit")) { bContinue = FALSE; continue; } } } cout << "Shutting down console" << endl; thisConsole->QuitOK(); return 0; };Now we can invoke a simple console server with the following code.
int main(void) { Console theConsole; theConsole.Initialize(); theConsole.Start(); Sleep(INFINITE); // this is not good! return EXIT_SUCCESS; }
HANDLE Console::GetCleanupHandle() const { return m_hCleanup; } int main(void) { Console theConsole; theConsole.Initialize(); theConsole.Start(); WaitForSingleObject(theConsole.GetCleanupHandle(), INFINITE); return EXIT_SUCCESS; }