ThreadLib

[ LiB ]

Almost every operating system (at least those that support multithreading) have built-in multithreading libraries. Unlike the Sockets API, however, the libraries aren't very compatible.

Therefore, you need to select a library according to the platform you're developing on. Win32 has threading built in, so that is a great help. Linux usually depends on the POSIX Threads library, more commonly known as pthreads . I will be using both of these libraries to create our own ThreadLib library.

Since threading libraries usually differ , I want to take you through the design of the ThreadLib library that I use for the book before I go on to show you how to use threads.

To start off, I'll show you the main functions:

  • Creating a thread

  • Killing a thread

  • Waiting for a thread to finish

  • Getting the thread's ID

  • Yielding a thread to the operating system

The entire library is enclosed within the ThreadLib namespace, by the way. Namespaces are a very handy way of avoiding naming conflicts with other pieces of code, and I explain them in much more detail in Appendix C, "C++ Primer" (found on the CD).

Headers and Typedefs

For starters, different thread libraries use the headers that are in different files. Depending on which compiler you are using, you'll want to include either windows .h (Win32) or pthread.h (Linux):

 #ifdef WIN32                // Windows 95 and above     #include <windows.h> #else                       // Linux     #include <pthread.h> #endif 

After that, I've got several typedefs, intended to make the library look cleaner. For example, I've got a function pointer typedef. (Again, see Appendix C on the CD if you are unfamiliar with function pointer typedefs.)

 typedef void (*ThreadFunc)(void*); 

This line defines a function pointer type that returns nothing (the first void) and takes a void pointer as its parameter. The type is named ThreadFunc . So what's the purpose of this? Whenever you create a new thread, you also need to tell it to execute a function. This typedef describes the kinds of functions that are to be executed as new threads.

The parameter void* allows you to pass in any single object as a parameter to the function. This is handy, because you can pass in a pointer to a number or a class, or you can even create your own collection class that has multiple data members , representing multiple arguments to a function. This will be demonstrated later on, when I show you how to create new threads.

Next on the agenda is the ThreadID typedef/class. Whenever you create a new thread, you need some way to reference it later on, in case you want to kill it, or wait for it to finish.

Let me start off by saying that Linux has taken the easy route and uses a single datatype to refer to threads pthread_t . Nice and easy.

Windows, on the other hand, is a pain in the butt. You see, a thread in Windows has two values associated with it. The first is a HANDLE to the thread object, which is the WIN32 API's funky version of a pointer. The only difference is that you can have many handles all with different values referring to the same object. Therefore, there is really no way to compare two different handles to see if they point to the same object. And, it gets better. It turns out that Windows keeps track of every handle it passes out, and if you don't close every handle you have open , Windows never deletes the object that the handle refers to. (ugh!) Not only that, but sometimes when you request a handle to a thread, it gives you a pseudo-handle , which, as far as I can tell, is good for nothing!

Because of this crazy behavior of Windows threads and the fact that you cannot get the original handle to a thread once you're in a thread, I've made a hack of sorts to make the library easier to use. Here are the typedefs for the ThreadID:

 #ifdef WIN32                // Windows 95 and above     typedef DWORD ThreadID;     std::map< DWORD, HANDLE > g_handlemap; #else                       // Linux     typedef pthread_t ThreadID; #endif 

On the WIN32 side of things, there are two lines of code: The ThreadID type is defined to be a DWORD , and a global std::map is created as well. WIN32 uses DWORDS to hold thread ID numbers , so that's what I'm going to use as well. The map may not be as obvious, though. If you're unfamiliar with maps, I cover them in more detail in Appendix C (on the CD), but let me just give you a quick rundown here. A map is a data structure that stores key-data pairs . A map assumes that whenever you have a piece of data you want to store, you also have a unique key that is related to the data. For example, if you live in the USA, a social security number could be a key referring to a person.

So, in this case, I have a unique key (thread ID number) referring to data that I want to store (the handle to the thread). Whenever the ThreadLib needs to find the handle of a thread, it looks it up in the table. That way, the user's program only needs to know about the thread ID number and never needs to mess with the handle. This implementation hides the ugly Win32 implementation from the user .

The Linux version is much simpler; it just uses the pthread_t datatype.

From here on, you can use the ThreadID datatype to manage the threads within your programs.

Creating a New Thread

I mentioned previously that whenever you create a new thread, it needs a function pointer to execute. In this section, however, I'm discussing the function signatures of the functions needed by the two threading APIs, and these are different.

NOTE

A function signature (sometimes referred to as footprint) is just a formal phrase that describes the parameters and the return type of a function. For example, the signature of int foo() means that it returns an integer and takes no parameters. The function int bar() has the same signature, but int baz( float blah ) has a different signature, since it has different parameters.

For example, the WIN32 threading library requires thread functions to be of the form

 DWORD WINAPI Function( void* ) 

This means that the function returns a DWORD (a typedef that represents a 32-bit integer), has the WINAPI function calling style, and takes a void* as its parameter. This is an advanced topic that I don't get to, so don't worry about it; all you need to know is that the keyword WINAPI is required in front of the function name .

On the other hand, pthreads requires functions that look like this:

 void* Function( void* ) 

This version returns a void* , has no function calling style, and takes a void* as its parameter. You may recall that I defined the ThreadFunc typedef to refer to functions of the form

 void Function( void* ) 

This means that the ThreadFunc typedef is almost the same as the pthread style, except it has no return value.

Ultimately, I want you to use functions of the last form with the threading library. This made my job a little harder, because I needed to come up with a clever way to make the library convert ThreadFunc functions into the native platform signature, which is not an easy task. I considered making a few #define macros to handle the differences between the return values and the calling style, but that method is messy. Instead, I've opted for a dummy function system (sometimes called a proxy system).

Figure 3.8 shows this dummy system. When a thread is created, it is told to call the appropriate dummy function for the current platform, which in turn calls the intended function.

Figure 3.8. The ThreadLib uses this dummy function system.

graphic/03fig08.gif


So whenever you want to create a thread, you pass in both the function you want to execute (in ThreadFunc form) and the parameters. In turn, the function packages the function and parameters into a dummy data structure, and passes it to the dummy function, which then executes the function you wanted.

ThreadLib::DummyData Structure

The dummy thread functions need to know two things: the function the dummy thread needs to call, and the data to pass into it. Since the dummy function can only accept a single void* as a parameter, you need to package these two things into a class of their own. Here is the DummyData class:

 class DummyData { public:     ThreadFunc m_func;     void* m_data; }; 

ThreadLib::DummyRun Function

Here is the dummy function that will run the function you want it to run:

 #ifdef WIN32 DWORD WINAPI DummyRun( void* p_data ) #else void* DummyRun( void* p_data ) #endif {     // convert the dummy data     DummyData* data = (DummyData*)p_data;     // run the function with the given data     data->m_func( data->m_data );     // now delete the data     delete data;     // and return 0.     return 0; } 

NOTE

Passing parameters into threads is a sticky business. You should always have a well-defined process to handle what happens to the parameters once the thread has executed. If you don't, you'll end up with memory leaks, and that's always a bad thing. This is also a reason to avoid killing threads prematurelyif the thread is supposed to delete its own parameters when it's finished with them, the thread won't get a chance to delete the parameters if you kill the thread manually.

The most interesting part of the code is the first five lines. The function signature defined depends on the system you are using. The rest of the function, however, is platform independent.

First the code casts the void* parameter into a DummyData* . The next command executes the m_func function pointer contained within the dummy data class, also passing it the m_data data.

Note that since a pointer is passed into the function, I need to allocate the DummyData class outside this function, and if I don't delete it, I'll have a memory leak. That works well for this purpose, because every call to the thread creation function creates a new dummy data class, and I can safely delete the class when I no longer need it.

ThreadLib::Create Function

After all this discussion, this section describes the part of the library that actually creates a new thread. For each operating system, you'll be calling a different function to create a thread ( CreateThread for Windows, pthread_create for Linux).

First, let's look at the function signature:

 inline ThreadID Create(ThreadFunc p_func, void* p_param ) 

The function returns a ThreadID , and its parameters are a ThreadFunc pointer and a void pointer. The function will create a new thread that will execute p_func using p_param as its argument.

Here's the first block of code from the function:

 ThreadID t;     // create a new dummy data block     DummyData* data = new DummyData;     data->m_func = p_func;     data->m_data = p_param; 

Basically, a new DummyData structure is created and set up.

 #ifdef WIN32    // create a WIN32 thread         HANDLE h;         h = CreateThread( NULL, 0, DummyRun, data, 0, &t );         if( h != 0 ) {             // insert the handle into the handlemap             g_handlemap[t] = h;         }     #else           // create a Linux thread         pthread_create( &t, 0, DummyRun, data );     #endif 

On the WIN32 side, a new HANDLE is created. This will store the handle of the thread after the CreateThread() function returns. CreateThread uses the parameters described in the following paragraph.

The first parameter is a pointer to a structure that describes security attributes for the thread. Since I'm not using any of these features, I just pass NULL into it, which tells the function to use the default attributes. The next parameter is the initial stack size of the thread in bytes. Since the operating system automatically resizes this anyway, I just pass 0 in. This tells the operating system to use the default size, which can vary with the system setup, but that doesn't really matter much. After that, I pass in a pointer to the DummyRun function and the dummy data structure as well. The fifth parameter is a collection of flags to the operating system, none of which are interesting enough to mention, and a value of 0 is sufficient. The final parameter is a pointer to the ThreadID type, and the function will put the thread ID number into it.

After the new thread is created, this function checks to see if the handle is valid (not zero). If the handle is valid, it needs to be inserted into the handle map. As you can see from the code, this is an easy task to accomplish. You can insert a key/data pair into a map just as you would use an array, where t is the key (you can think of it as an index of an array, but it isn't actually an array), and h is the data to store in the table.

On the pthreads side of things, the function is similar, but simpler as well. The first parameter is a pointer to the thread ID so that it can be filled out when the function is complete, and the second parameter accepts creation flags, which are of little interest to us. The final two parameters are the function pointer and the dummy data structure.

Here's the final block of code:

 if( t == 0 ) {         // delete the data first         delete data;          // throw an error         throw Exception( CreationFailure );     }     return t; } 

This code checks to see if the thread ID is valid (any number except 0). If the ID is 0, the thread creation failed, so steps need to be taken to report the error and prevent memory leaks. As you can see, this block of code deletes the dummy data that was created earlier and throws a ThreadLib::Exception of type CreationFailure . I prefer exceptions because they greatly simplify code and don't usually clutter it up as traditional error checking code does. If you're not familiar with exceptions, see Appendix C, found on the CD.

Finally, the ID of the thread is returned, and you now have a new thread!

Getting the ID of a Thread

When you're in a thread executing code, you may need to find out which thread you are in by using the simple functions for obtaining the ID that are shown here:

 inline ThreadID GetID() {     #ifdef WIN32         return GetCurrentThreadId();     #else         return pthread_self();     #endif } 

On the WIN32 side of things, you can call the GetCurrentThreadID() function to get the thread ID. The Linux version uses the pthread_self() function.

Waiting for a Thread to Finish

Sometimes you'll want to be able to stop and wait for a thread to finish. This is usually helpful if you want to run parallel threads and have the main thread wait until the parallel threads are finished. This is a relatively easy thing to do.

In Windows, all you need to do is call the WaitForSingleObject() function with the threads handle, and the function will wait until the thread is done. Linux has a similar function, called pthread_join() , which joins the thread you want to finish with the current one. The two functions do the same thing, really.

 inline void WaitForFinish( ThreadID p_thread ) {     #ifdef WIN32         // look up the handle and wait for the thread to finish         WaitForSingleObject( g_handlemap[p_thread], INFINITE );         // close the handle of the thread         CloseHandle( g_handlemap[p_thread] );         // remove the handle from the map         g_handlemap.erase( p_thread );     #else         // "join" the thread. This essentially transfers control over to         // the thread and waits for it to finish.         pthread_join( p_thread, NULL );     #endif } 

For the WIN32 code, the WaitForSingleObject() function needs a handle to the thread you want to wait for. Since the function accepts a ThreadID as its parameter, and not a handle, you'll need to consult the global handle map to look up the handle for the thread. Since maps are nice and easy to use, you can access a handle just as you would an array, using this code: g_handlemap[p_thread] . The code returns the handle of the thread. Pretty simple, don't you think? The second parameter of the function is how long you want to wait; you could wait for only a specified time, but I rarely need that kind of flexibility, so I just enter INFINITE as the time parameter. Of course, if the thread never finishes, you could run into some zombie-thread problems, but if you design things well, you shouldn't.

NOTE

Since WIN32 threads aren't actually removed from the system until you close the handle, you should try to call the WaitForFinish() functions on your threads, even if you know they are finished, just so the functions can close the handle. It's good program ming practice, since you really shouldn't just launch a new thread and hope that it will finish correctly.

After you've waited for the thread, you can be certain that the thread is completed, so you should close the handle of the thread, and delete the handle from the handle map.

The Linux version is easy. It accepts the thread number and a pointer to some attributes that I'm not really concerned about, so I just pass NULL instead.

Killing a Thread

Sometimes, although rarely, you'll need to kill a thread outright . It's not recommended that you do so, however, because killing threads tends to lead to all sorts of problems, most notably memory leaks. However, you're still allowed to do so. Here's the code to kill a thread:

 inline void Kill( ThreadID& p_thread ) {     #ifdef WIN32         // terminate the thread         TerminateThread( g_handlemap[p_thread], 0 );         // close the handle of the thread         CloseHandle( g_handlemap[p_thread] );         // remove the handle from the map         g_handlemap.erase( p_thread );     #else         // cancel the thread.         pthread_cancel( p_thread );     #endif } 

As with the WaitForFinish() function, the WIN32 TerminateThread() function also requires a handle, so it is looked up in the handle map and passed into the function. The second parameter specifies the return value of the function. It's not very useful for what I'm doing here, so I just use 0.

After the thread is terminated , the function works the same way as the WaitForFinish() function: It closes the handle and erases it from the handle map.

The Linux version simply calls the pthread_cancel() function to cancel the thread.

Yielding a Thread

Earlier, I told you about putting a thread to sleep. This is a useful function for a threading library, so that you can play nicely with the operating system.

I use the Sleep function of Windows, and Linux's usleep function to tell the threading system to put the current thread to sleep.

There's one little "gotcha," though; usleep accepts the sleep time in microseconds , rather than the usual milliseconds . Don't ask me why the creators of the function decided they needed that kind of resolution, but that's just how it is. I use a default of 1 millisecond for my function, but you can pass in anything you like:

 inline void YieldThread( int p_milliseconds = 1 ) {     #ifdef WIN32         Sleep( p_milliseconds );     #else         usleep( p_milliseconds * 1000 );     #endif } 

[ LiB ]


MUD Game Programming
MUD Game Programming (Premier Press Game Development)
ISBN: 1592000908
EAN: 2147483647
Year: 2003
Pages: 147
Authors: Ron Penton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net