Journal Articles
Browse in : |
All
> Journals
> Overload
> 35
(8)
All > Topics > Programming (877) Any of these categories - All of these categories |
Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.
Title: Standard C++
Author: Administrator
Date: 26 January 2000 16:50:56 +00:00 or Wed, 26 January 2000 16:50:56 +00:00
Summary:
Body:
Shortly after the C++ standard was announced I read an article explaining what the standardisation committee will do next. There is still work to do, it said. Although the standard is not allowed to change for 7 years some parts will need to be clarified and ambiguities and inconsistencies will need to be resolved. At some stage the committee will also have to consider the next C++ standard. The article urged its readers to report parts of the existing standard that could be improved and to submit suggestions for its next incarnation.
I thought about this for a while. It seems to me that the biggest problem with C++ is its sheer size and complexity. Commercial organisations need to spend a lot of time and money training C++ programmers. Many of them feel it isn't worth the investment. What's wrong with C, Java or Visual Basic? Or simply C++ as a "better C"? To put it another way, C++ is an excellent language for writing efficient, high-quality, low-level software. But most software isn't like that. So, if C++ is to remain a main-stream language, it needs to become smaller, simpler and easier to use - without losing any of its power and flexibility.
Unfortunately, but I had no idea how this could be done. So I thought some more and decided that the standard could be improved by making it bigger. Like C before it, C++ assumes an underlying (abstract) machine. It is a single-processor, single-process, single-thread machine. And most modern software is written to run in an environment of networked machines running multi-threading, multi-processing operating systems. If standard C++ doesn't provide support for such environments C++ programs will always be platform-specific and the benefits of having a standard will inevitably be eroded.
So, I asked myself, "What other facilities do we need in C++ to support modern object-oriented, distributed-processing software environments?". The answer I gave myself was this:
-
threads and thread synchronisation mechanisms;
-
processes, inter-process communications and process synchronisation mechanisms;
-
networking and support for distributed objects.
I thought of this as a hierarchy of architectures, each layer adding facilities to those of the layer beneath. For completeness, I included support for objects in the hierarchy and produced this table.
Architecture | Objects | Threads | Virtual Address Spaces (processors) | Physical Address Spaces (processors) |
procedural | 0 | 1 | 1 | 1 |
object oriented | n | 1 | 1 | 1 |
multi-threaded | n | n | 1 | 1 |
multi-processing | n | n | n | 1 |
distributed | n | n | n | n |
Traditional languages, like C, address the procedural level. Object-oriented languages, like C++ and Java, address the object-oriented level.
Multi-threading, multi-processing and distributed architectures are not directly supported by general purpose programming languages[1]. That is provided by operating systems and libraries. Perhaps it is time to think about adding these facilities to the C++ Standard Library.
Since then I have been prompted to consider multi-threading support for other reasons, too. Recent articles in Overload [Kelly31], [Kelly32] and the C/C++ Users Journal [Harrington] have described designs that differ from those I've seen before. There are also unresolved issues in projects at work related to the use of threads in object-oriented designs.
So, I set myself the task of exploring current ideas and trying to design thread support facilities suitable for inclusion in the standard library. I am aware that this topic has been considered by a number of people more knowledgeable and more able than myself. And I believe that threads were omitted from the standard library partly because existing threading models differ too much for a 'standard' solution to be accepted. The aim was not so much to define an extension to the standard, but more to learn about the strengths and weaknesses of different approaches while remaining focused on the essentials.
I looked at a number of software packages providing threads.
They are listed in Table 1.
Category | Package | Description |
Operating system kernel API | Win32 Threads | C functions |
Posix Threads | C functions | |
Java interface | Java Threads | Java Classes |
General-purpose C++ interface | Rogue Wave Threads | C++ classes |
Objectspace Thread Classes | C++ classes | |
ACE Thread classes | C++ classes | |
John Harrington's Thread classes | C++ classes | |
Allan Kelly's Class Templates | C++ class templates | |
Bio-Rad's Thread Classes | C++ classes | |
Vendor-specific C++ interface | MFC CThread Class | C++ classes |
OWL TThread Class | C++ classes | |
VCL TThread Class | C++/Delphi classes |
Table 1. A Selection of Thread packages
The packages fall into three main categories: operating system kernel API, general-purpose C++ interface and vendor-specific interface. In Java the distinction between operating system and language is blurred, so that has been given a category of its own.
Most of the packages listed here have been studied only superficially. The kernel APIs provide the low-level facilities that might be used to implement C++ classes. They were examined to see what building blocks are readily available. Each of the vendor-specific C++ interfaces depends on facilities not directly related to the provision of multi-threading (e.g. Window objects). I regarded these extra facilities as inessential and inappropriate to a standard library. The Java and general-purpose C++ interface packages have much in common and I concentrated on these. It is worth noting, however, that the vendor-specific packages, when stripped of extraneous features, also have much in common with the Java and general-purpose groups.
I will call the features common to most/all of the general-purpose interfaces (including Java) the core facilities.
Table 2 lists the core facilities together with the corresponding Win32 API functions by way of example. These features define the thread model that will be discussed in this article.
Description | Win32Function |
Create a new thread | CreateThread |
Terminate the thread function. | ExitThread |
Get a thread's exit code. | GetExitCodeThread |
Destroy a thread. | CloseHandle |
Get a thread's priority. | GetThreadPriority |
Change a thread's priority. | SetThreadPriority |
Suspend execution of the thread function until resumed. | SuspendThread |
Resume execution of the thread function. | ResumeThread |
Suspend execution until some condition becomes true. | WaitForMultipleObjects, etc. |
Suspend execution of the thread function for a specific time period. | Sleep |
Get the handle of the currently executing thread. | GetCurrentThread |
Get the ID of the currently executing thread. | GetCurrentThreadId |
Wait for the thread function to finish. | WaitForSingleObject |
Table 2. Summary of Core Facilities
In considering these facilities it is important to distinguish between a thread and the function it is executing. A thread is a resource provided by the system; a thread function is a user-defined piece of code.
Other facilities provided by existing implementations include things like: names for threads (useful for diagnostics), access control (does the user have permission to create a thread, change its priority, etc.), ordering relations (operator<(), operator==(), etc.) and support for thread groups. Support for miscellaneous thread facilities and extensions is not discussed here.
The purpose of a thread is to execute a user-defined function asynchronously with respect to other thread functions. And the first design challenge is to provide a way of running thread functions with widely varying interfaces in a thread that accepts only one, simple thread function interface. I call this the function signature mismatch problem.
Ideally, we would like the thread library implementation to call an arbitrary, user-defined function, but the C++ language has no construct that lets us call a function with an unknown parameter list. So either we restrict the type of function we can call or we must find an indirect way of calling any given function.
I shall consider three approaches:
-
Thread-Runs-Function,
-
Thread-Is-Polymorphic-Object,
-
Thread-Runs-Polymorphic-Object.
The first two are wide-spread; the third is the one we use at Bio-Rad. The code fragments in Thread-Runs-Function through Thread-Runs-Polymorphic-Object show the characteristic features of each approach.
class thread { public: typedef int function (void*); // create thread and start thread function thread (function*, void*); . . . };
The thread class in the Thread-Runs-Function approach is a thin wrapper around the thread functions in the kernel API. It leaves responsibility for solving the function signature mismatch problem entirely in the hands of the programmer. The user is expected to provide a non-member function (or static member function) with the same signature as the underlying system call. The exact signature of the thread function varies from platform to platform, but typically the thread function takes a void* parameter and returns an integer value (signed or unsigned, int or long).
This is acceptable for procedural-style programming, but less useful for object-oriented designs. Non-member functions are usually inappropriate in O-O designs and adding static member functions just to fit the thread class interface complicates the implementation. And, of course, the use of a void* parameter introduces the risk of unsafe type conversions.
class thread { public: // create thread, destroy thread thread(); virtual ~thread(); // start thread function void start(); . . . protected: // thread function implementation virtual int run() = 0; . . . };
Thread-Is-Polymorphic-Object fits better with the object-oriented style. The static member function is replaced with a virtual member function and the void* parameter is replaced with the implicit this pointer.
The library user must still provide an implementation for a function with a simple, fixed interface - the virtual run() function. However, it is natural to provide any data required by the run() function as data members of the derived class. The absence of a parameter list does not adversely affect the design and the resulting code is elegant and type-safe. The extra level of indirection provided by the virtual function call helps the application developer to solve the function signature mismatch problem.
The need to sub-class thread adds complexity to the class hierarchies, but this is a small price to pay for the extra convenience and type safety. In a sense, this is what object-oriented programming is all about. However, with this approach the thread management functions are tightly coupled with the application logic and this makes it more difficult to change the application's threading strategy.
class thread { public: struct function; // create thread and start thread // function thread (function&); . . . }; struct thread::function{ // destroy thread function virtual ~function() {} // thread function implementation virtual int run() = 0; };
The Thread-Runs-Polymorphic-Object approach retains the main characteristics of Thread-Is-Polymorphic-Object, but also separates the concepts of thread and thread function into different classes. The thread class handles thread management, while the thread function class encapsulates the application logic.
Here we have an example of the Command design pattern [GoF]. The thread class performs the role of the Invoker and the thread function class performs the role of the abstract Command class. It is the responsibility of the application programmer to provide a class derived from thread::function to perform the ConcreteCommand role. As noted in the Gang-of-Four book, a Command is an object-oriented replacement for a call-back function.
For the Command pattern to work it has to solve the function signature mismatch problem. The abstract Command must provide a simple, fixed execution interface for the Invoker to use. At the same time its implementation must be able to execute an arbitrary action function. It is the ConcreteCommand class that translates the abstract operation into a concrete method call.
The pattern represents a complete solution to the function signature mismatch problem, but the thread library can only provide part of it. The application developer must still provide a concrete class to implement the run() function.
Designs based on templates are also possible, as described by Allan Kelly in Overloads 31 and 33 ([Kelly31], [Kelly33]). However, using a class template doesn't solve the function signature mismatch problem. Even if the thread function type is supplied as a template parameter, the template functions must still assume a fixed interface. In Allan Kelly's articles, for example, the thread function is assumed to be a class with a Run() member function.
Templates may also lead to code bloat. Some of the code in Allan's thread template is independent of the template parameters and the compiler will duplicate that code for every instantiation of the template. This effect can be controlled by techniques such as factoring out the common behaviour into a non-template base class, using member templates and partial specialisations, but even these techniques are not sufficient here because the duplication occurs within the member functions.
There is a practical problem associated with templates, too. Some compilers still require template function bodies to be placed in the header file and this forces platform-dependent details into the headers. On Win32 systems, for example, it means a #include "Windows.h" in the thread.h file to provide definitions of the HANDLE type, the CreateThread() function, etc.. This leads to #if directives that clutter the headers and long compilation times. Even worse, in my view, it turns what was supposed to be a standard (i.e. non-proprietary) header into one that depends on every platform it supports!
Templates are not all bad, though. Supplying the thread function type as a template parameter removes the need to inherit from a base class and override a virtual function. On the other hand, polymorphism provides its own benefits - late binding allows the dynamic type of the thread function to be determined at run time and this flexibility can be important.
As a concrete example, suppose we want to print a file in the background using the following function:
void Printer::print ( std::string file_name);
If our thread library uses the Thread-Runs-Function design we must first create a thread function. We also need a simple struct to pass the printer and file name information to the thread function via its single void* parameter. The print function must decode this structure before calling Printer::print().
// thread function arguments struct print_args { Printer* printer; std::string* file_name; }; // thread function int print_function (void* address){ print_args* args = static_cast<print_args*>(address); Printer* printer = args->printer; std::string* file_name = args->file_name; printer->print(*file_name); return 0; }
The new print function can be run in a separate thread like this:
Epson_Stylus printer; std::string file_name("printer_test.txt"); print_args args ={&printer, &file_name}; thread print_thread(print_function,&args);
Using Thread-Runs-Polymorphic-Object we must create a concrete thread function class:
// concrete thread function class class print_function : public thread::function { public: print_function(Printer& p, std::string n) : printer(p), file_name(n) {} private: virtual int run() { printer.print(file_name); return 0; } Printer& printer; std::string file_name; };
Like the print_args struct in the Thread-Runs-Function design the print function class holds information about the printer and the file to be printed. Now, though, the virtual thread function has access to that data via the implicit this pointer. No cast is necessary and no statements are required to decode the data structure.
The new thread class can be used like this:
Epson_Stylus printer; std::string file_name("printer_test.txt"); print_function print_file(printer,file_name); thread print_thread(print_function);
The quantity of code is similar in both designs, but the object-oriented style leads to better quality.
So far, we have only considered the fundamental mechanism for invoking a user-defined function in a separate thread. Most of the core facilities have not been discussed at all and some of them raise further design issues. There is one problem that is common to many of the core facilities; it is the age old problem of how to balance the need for a simple, consistent interface with the desire to make available the full power of each underlying implementation.
As an example of what I mean, a VGA interface can be provided for just about any video card and this is fine for some applications. But many applications need more advanced graphical operations. Of course there are plenty of video cards to choose from, but they differ in capabilities and native interface. How can we write a portable graphics library that takes advantage of the particular features of each card? Threads present us with several examples of this sort of dilemma and I would like to explore these in the next article.
Then, beyond the basic thread facilities there are issues of ownership and synchronisation. In each of the thread designs presented in this article a parent thread function provides some data to the child. Sometimes responsibility for those data items remains with the parent, sometimes it is passed on to the child. Similarly, a responsible parent will clean up after a child thread dies, but some parents will die themselves, leaving their children orphaned. I suggest these issues are best addressed by a more application-oriented threading model and there is scope for another article here.
This article has described three common approaches to basic thread facilities and discussed their suitability for inclusion in a standard library. The Thread-Runs-Polymorphic-Object model fits well with object-oriented designs and separates the thread support facilities from the application-specific code.
Providing access to platform-specific facilities and the use of application-oriented thread models were raised as topics for further discussion.
[Harrington] C/C++ Users Journal, Vol. 17 No. 8, August 1999, "Win32 Multithreading Made Easy" by John Harrington.
[1] By "general purpose programming languages" I mean languages that don't have their own environment. This includes C and C, but not Smalltalk and Forth. I'm not sure how to classify Java!
Notes:
More fields may be available via dynamicdata ..