If you were wondering what local classes were good for, here's a good example that makes easy work of spawning single-statement threads.
Most of todays multithread-capable application frameworks provide an abstract Thread base class with a virtual member function, usually called run, that becomes the mainline of the calling thread. This is accomplished by creating the thread and then calling the run function from within the newly created thread.
The thread class wrappers are implemented in terms of the platforms native APIs for thread creation. Most native thread APIs provide a thread creation function that accepts a pointer to a function that will be called as the mainline of the new thread, and a user-defined parameter that will be passed to that mainline, usually as a void * pointer. The following example demonstrates how the most common wrapper classes use the APIs for thread creation:
class Thread { public: Thread() { m_threadID = createThread (runWrapper, this); } virtual ~Thread() { destroyThread(m_threadID); } protected: virtual void run() = 0; private: static void runWrapper (Thread* thread){ thread->run(); } ... };In this case the class that extends the thread wrapper class and implements the run routine must be able to handle the arguments that will be passed to the thread. Those arguments are usually member variables of the class that form the context in which the thread will run. The following piece of code demonstrates how arguments are passed to the new thread:
class PrintAsynch: public Thread { public: PrintAsynch (const std::string& message) : m_message(message) {} protected: virtual void run() { std::cout << m_message; } private: std::string m_message; };This article will show another technique for creating threads using thread wrapper classes that eliminates the need to derive from a common Thread base class. The asynch macro described below accepts any statement that can be passed to a macro and runs it in a new thread. For example, instead of defining a PrintAsynch class as shown above, the same message can be printed in a new thread using the following statement inside a function:
asynch0 (std::cout << "This is a sample message.";);Although you do have to play a little trick (described later) to use statements containing commas, this technique is handy, say, when you have a simple asynchronous task to perform and you dont want to deal with the underlying thread API.
The task above is performed using local classes defined in a lexical block. The class that is defined is accessible only in the function block where it resides. The following piece of code demonstrates how this is done:
while (true) { class Local { // define your class here }; };The idea behind the asynch0 macro is to automate the idea described in the beginning of the article (deriving from a Thread class) by defining a class that derives from Thread in its own block. The following snippet shows how this is done. (The full definition of the macros is shown in Listing 1.)
#define asynch0(statement)\ {\ class DummyThread: public Thread\ {\ virtual void run()\ {\ statement\ delete this;\ }\ };\ new DummyThread;\ }The class DummyThread is defined in a separate block to separate it from the parent namespace. This is done to avoid any name collision if multiple asynch0 statements appear in the same block:
asynch0( cout << "World."; ); asynch0( cout << "Hello "; );Because the DummyThread class is defined in a separate block, no name collision will occur. You can also use the macro recursively; for example:
asynch0(asynch0(...(statement)...);This will produce multiple classes named DummyThread, each in its own nested scope. This wont create a conflict since each nested DummyThread class will be defined in a member function of the enclosing DummyThread class. The following code demonstrates this situation:
class DummyThread { ... virtual void run() { ... class DummyThread {...}; ... } ... };When using the asynch macro, the statement that is to be executed asynchronously cannot use local variables whose addresses change each time the routine is entered, such as auto variables declared before the DummyThread declaration. The following code demonstrates this situation:
int main() { int localVar; class DummyThread: public Thread { virtual void run() { localVar = 10; } // impossible, localVar cannot // be accessed here }; }If you want to use local variables as parameters to the asynch macro, you could declare them as static instead of auto variables, with the drawback that the enclosing function is now not reentrant or suitable for multithreaded use. Unfortunately, one of the most commonly used C++ compilers Microsoft C++ 6.0 SP3 does not support accessing local static variables from local classes. For example, if you declare localVar from the example above as static, you will still get an error that localVar cannot be accessed.
If you want to pass non-static local variables to the statement that will be executed asynchronously, you should first store them in a variable that is accessible from the locally defined class, such as a static local variable, or a globally defined one, or the best choice a member variable of the class DummyThread. The following code shows how this is done:
int main(int argc, char* argv[]) { { class DummyThread: public Thread { public: DummyThread(int argc, char** argv) : argc(argc), argv(argv) {} protected: virtual void run() {...} private: int argc; char** argv; } new DummyThread(argc, argv); } }This is a kind of marshaling of parameters to the new thread. If the routine that uses the asynch macros can be called sequentially and concurrently you should be careful what variables you are marshaling. You should be aware about the lifetime of the variables and of the thread. For instance, a common mistake is to use a local variable that can be destroyed before the new thread is created. Here is a common situation:
void func(int param) { asynch1(param = 10;, int&, param); }In this case the reference expires immediately as the func routine exits. There is no simple way to put variable arguments in a macro, so the expansion of the asynch macro that can marshal arguments will be defined as:
asynchX(statement, type1, param1, ..., typeX, paramX)where X is the number of parameters that will be marshaled.
If the macro is implemented as described above you cannot be sure when the threads created with asynch will end, so it is hard to synchronize the threads in your application. You should only use statements that wont produce any unspecified behavior if terminated abruptly when the application exits and that do not need any synchronization. A solution for this problem can be to expand the macro with one additional parameter that will receive a pointer to the wrapper class instance of the new thread. Then you can use that pointer for operations on the thread such as waiting, suspending, resuming, or terminating. The following piece of code shows how this is done:
#define asyncht0(statement, thread)\ // the body is defined in listing 1 ... static void runWrapper(Thread *param) { param->run(); // do not destroy the object, // let the user do it } ... thread = new DummyThread();\ }The statement that can be passed as a parameter to the asynchX macros has the following restrictions due to specific features of the preprocessor. Statements containing commas enclosed in parentheses () or single or double quotation marks ('' or "") are valid macro arguments. The following constructions are correctly preprocessed:
asynch0(std::cout << "Hello, world" << ',' << log(2, 2)); asynch0(for(int a = 0, b = 0; a < 10, b < 10; a++, b++););Statements with commas that are not enclosed in the specific symbols as mentioned above cannot be passed to the asyncX macro directly. The following usages of the asynchX macro do not work:
asynch0(int a, b;); asynch0(std::map<int, int> m;);A solution to this problem is to define a symbol that can replace the comma in macro parameters. The following construction shows how this is done:
#define comma ,Now the unusable statments shown above can be corrected using the comma define instead of directly writing ,:
asynch0(int a comma b;); asynch0(std::map<int comma int> m;);A shortcomming of the asynchX macros is that you will have to use the comma define in more complicated constructions. This can make your code less readable and hardly supportable if not enough documentation is supplied.
Summary
The advantages of the asynch macro are as follows: first, you should not have to manually implement a thread routine as you should do when using low-level APIs or high-level APIs such as the _beginthreadex routine in the MSVC runtime library or MFCs CreateThread. Second, you are not bound to the fixed arguments in those thread routines, usually a single void * parameter. Third, you are free to choose the arguments that will be used without any preliminary routine or class declarations. Using the asynch macro for execution of functions in a new thread is simple and elegant.
Kalin Nakov is a project manager at WizCom (http://www.wizcom.net), a software solutions company. He is interested in object-oriented technologies and innovations in this field together with system programming for Windows and Unix. He can be contacted at me@kalinnakov.com.