C H A P T E R  11

Building Multithreaded Programs

This chapter explains how to build multithreaded programs. It also discusses the use of exceptions, explains how to share C++ Standard Library objects across threads, and describes how to use classic (old) iostreams in a multithreading environment.

For more information about multithreading, see the Multithreaded Programming Guide, the Tools.h++ User's Guide, and the Standard C++ Library User's Guide.


11.1 Building Multithreaded Programs

All libraries shipped with the C++ compiler are multithreading-safe. If you want to build a multithreaded application, or if you want to link your application to a multithreaded library, you must compile and link your program with the -mt option. This option passes -D_REENTRANT to the preprocessor and passes -lthread in the correct order to ld. For compatibility mode (-compat[=4]), the -mt option ensures that libthread is linked before libC. For standard mode (the default mode), the -mt option ensures that libthread is linked before libCrun.

Do not link your application directly with -lthread because this causes libthread to be linked in an incorrect order.

The following example shows the correct way to build a multithreaded application when the compilation and linking are done in separate steps:


example% CC -c -mt myprog.cc
example% CC -mt myprog.o

The following example shows the wrong way to build a multithreaded application:


example% CC -c -mt myprog.o
example% CC myprog.o -lthread <- libthread is linked incorrectly

11.1.1 Indicating Multithreaded Compilation

You can check whether an application is linked to libthread or not by using the ldd command:


example% CC -mt myprog.cc
example% ldd a.out
libm.so.1 =>      /usr/lib/libm.so.1
libCrun.so.1 =>   /usr/lib/libCrun.so.1
libthread.so.1 => /usr/lib/libthread.so.1
libc.so.1 =>      /usr/lib/libc.so.1
libdl.so.1 =>     /usr/lib/libdl.so.1

11.1.2 Using C++ Support Libraries With Threads and Signals

The C++ support libraries, libCrun, libiostream, libCstd, and libC are multithread safe but are not async safe. This means that in a multithreaded application, functions available in the support libraries should not be used in signal handlers. Doing so can result in a deadlock situation.

It is not safe to use the following in a signal handler in a multithreaded application:


11.2 Using Exceptions in a Multithreaded Program

The current exception-handling implementation is safe for multithreading; exceptions in one thread do not interfere with exceptions in other threads. However, you cannot use exceptions to communicate across threads; an exception thrown from one thread cannot be caught in another.

Each thread can set its own terminate() or unexpected() function. Calling set_terminate() or set_unexpected() in one thread affects only the exceptions in that thread. The default function for terminate() is abort() for any thread (see Section 8.2, Specifying Runtime Errors).

11.2.1 Thread Cancellation

Thread cancellation through a call to pthread_cancel(3T) results in the destruction of automatic (local nonstatic) objects on the stack except when you specify -noex or -features=no%except.

pthread_cancel(3T)uses the same mechanism as exceptions. When a thread is cancelled, the execution of local destructors is interleaved with the execution of cleanup routines that the user has registered with pthread_cleanup_push(). The local objects for functions called after a particular cleanup routine is registered are destroyed before that routine is executed.


11.3 Sharing C++ Standard Library Objects Between Threads

The C++ Standard Library (libCstd -library=Cstd) is MT-Safe, with the exception of some locales, and it ensures that the internals of the library work properly in a multi-threaded environment. You still need to lock around any library objects that you yourself share between threads. See the man pages for setlocale(3C) and attributes(5).

For example, if you instantiate a string, then create a new thread and pass that string to the thread by reference, then you must lock around write access to that string, since you are explicitly sharing the one string object between threads. (The facilities provided by the library to accomplish this task are described below.)

On the other hand, if you pass the string to the new thread by value, you do not need to worry about locking, even though the strings in the two different threads may be sharing a representation through Rogue Wave's "copy on write" technology. The library handles that locking automatically. You are only required to lock when making an object available to multiple threads explicitly, either by passing references between threads or by using global or static objects.

The following describes the locking (synchronization) mechanism used internally in the C++ Standard Library to ensure correct behavior in the presence of multiple threads.

Two synchronization classes provide mechanisms for achieving multithreaded safety; _RWSTDMutex and _RWSTDGuard.

The _RWSTDMutex class provides a platform-independent locking mechanism through the following member functions:

The _RWSTDGuard class is a convenience wrapper class that encapsulates an object of _RWSTDMutex class. An _RWSTDGuard object attempts to acquire the encapsulated mutex in its constructor (throwing an exception of type ::thread_error, derived from std::exception on error), and releases the mutex in its destructor (the destructor never throws an exception).


class _RWSTDGuard
{
public:
    _RWSTDGuard (_RWSTDMutex&);
    ~_RWSTDGuard ();
};

Additionally, you can use the macro _RWSTD_MT_GUARD(mutex) (formerly _STDGUARD) to conditionally create an object of the _RWSTDGuard class in multithread builds. The object guards the remainder of the code block in which it is defined from being executed by multiple threads simultaneously. In single-threaded builds the macro expands into an empty expression.

The following example illustrates the use of these mechanisms.


#include <rw/stdmutex.h>
 
//
// An integer shared among multiple threads.
//
int I;
 
//
// A mutex used to synchronize updates to I.
//
_RWSTDMutex I_mutex;
 
//
// Increment I by one. Uses an _RWSTDMutex directly.
//
 
void increment_I ()
{
   I_mutex.acquire(); // Lock the mutex.
   I++;
   I_mutex.release(); // Unlock the mutex.
}
 
//
// Decrement I by one. Uses an _RWSTDGuard.
//
 
void decrement_I ()
{
   _RWSTDGuard guard(I_mutex); // Acquire the lock on I_mutex.
   --I;
   //
   // The lock on I is released when destructor is called on guard.
   //
}


11.4 Using Classic Iostreams in a Multithreading Environment

This section describes how to use the iostream classes of the libC and libiostream libraries for input-output (I/O) in a multithreaded environment. It also provides examples of how to extend functionality of the library by deriving from the iostream classes. This section is not a guide for writing multithreaded code in C++, however.

The discussion here applies only to the old iostreams (libC and libiostream) and does not apply to libCstd, the new iostream that is part of the C++ Standard Library.

The iostream library allows its interfaces to be used by applications in a multithreaded environment by programs that utilize the multithreading capabilities when running supported versions of the Solaris operating system. Applications that utilize the single-threaded capabilities of previous versions of the library are not affected.

A library is defined to be MT-safe if it works correctly in an environment with threads. Generally, this "correctness" means that all of its public functions are reentrant. The iostream library provides protection against multiple threads that attempt to modify the state of objects (that is, instances of a C++ class) shared by more than one thread. However, the scope of MT-safety for an iostream object is confined to the period in which the object's public member function is executing.



Note - An application is not automatically guaranteed to be MT-safe because it uses MT-safe objects from the libC library. An application is defined to be MT-safe only when it executes as expected in a multithreaded environment.



11.4.1 Organization of the MT-Safe iostream Library

The organization of the MT-safe iostream library is slightly different from other versions of the iostream library. The exported interface of the library refers to the public and protected member functions of the iostream classes and the set of base classes available, and is consistent with other versions; however, the class hierarchy is different. See Section 11.4.2, Interface Changes to the iostream Library for details.

The original core classes have been renamed with the prefix unsafe_. TABLE 11-1 lists the classes that are the core of the iostream package.


TABLE 11-1 iostream Original Core Classes

Class

Description

stream_MT

The base class for MT-safe classes.

streambuf

The base class for buffers.

unsafe_ios

A class that contains state variables that are common to the various stream classes; for example, error and formatting state.

unsafe_istream

A class that supports formatted and unformatted conversion from sequences of characters retrieved from the streambufs.

unsafe_ostream

A class that supports formatted and unformatted conversion to sequences of characters stored into the streambufs.

unsafe_iostream

A class that combines unsafe_istream and unsafe_ostream classes for bidirectional operations.


Each MT-safe class is derived from the base class stream_MT. Each MT-safe class, except streambuf, is also derived from the existing unsafe_ base class. Here are some examples:


class streambuf: public stream_MT {...};
class ios: virtual public unsafe_ios, public stream_MT {...};
class istream: virtual public ios, public unsafe_istream {...};

The class stream_MT provides the mutual exclusion (mutex) locks required to make each iostream class MT-safe; it also provides a facility that dynamically enables and disables the locks so that the MT-safe property can be dynamically changed. The basic functionality for I/O conversion and buffer management are organized into the unsafe_ classes; the MT-safe additions to the library are confined to the derived classes. The MT-safe version of each class contains the same protected and public member functions as the unsafe_ base class. Each member function in the MT-safe version class acts as a wrapper that locks the object, calls the same function in the unsafe_ base class, and unlocks the object.



Note - The class streambuf is not derived from an unsafe class. The public and protected member functions of class streambuf are reentrant by locking. Unlocked versions, suffixed with _unlocked, are also provided.



11.4.1.1 Public Conversion Routines

A set of reentrant public functions that are MT-safe have been added to the iostream interface. A user-specified buffer is an additional argument to each function. These functions are described as follows.


TABLE 11-2 MT-Safe Reentrant Public Functions

Function

Description

char *oct_r (char *buf,

int buflen,

long num,

int width)

Returns a pointer to the ASCII string that represents the number in octal. A width of nonzero is assumed to be the field width for formatting. The returned value is not guaranteed to point to the beginning of the user-provided buffer.

char *hex_r (char *buf,

int buflen,

long num,

int width)

Returns a pointer to the ASCII string that represents the number in hexadecimal. A width of nonzero is assumed to be the field width for formatting. The returned value is not guaranteed to point to the beginning of the user-provided buffer.

char *dec_r (char *buf,

int buflen,

long num,

int width)

Returns a pointer to the ASCII string that represents the number in decimal. A width of nonzero is assumed to be the field width for formatting. The returned value is not guaranteed to point to the beginning of the user-provided buffer.

char *chr_r (char *buf,

int buflen,

long num,

int width)

Returns a pointer to the ASCII string that contains character chr. If the width is nonzero, the string contains width blanks followed by chr. The returned value is not guaranteed to point to the beginning of the user-provided buffer.

char *form_r (char *buf,

int buflen,

long num,

int width)

Returns a pointer of the string formatted by sprintf, using the format string format and any remaining arguments. The buffer must have sufficient space to contain the formatted string.




Note - The public conversion routines of the iostream library (oct, hex, dec, chr, and form) that are present to ensure compatibility with an earlier version of libC are not MT-safe.



11.4.1.2 Compiling and Linking With the MT-Safe libC Library

When you build an application that uses the iostream classes of the libC library to run in a multithreaded environment, compile and link the source code of the application using the -mt option. This option passes -D_REENTRANT to the preprocessor and -lthread to the linker.



Note - Use -mt (rather than -lthread) to link with libC and libthread. This option ensures proper linking order of the libraries. Using -lthread improperly could cause your application to work incorrectly.



Single-threaded applications that use iostream classes do not require special compiler or linker options. By default, the compiler links with the libC library.

11.4.1.3 MT-Safe iostream Restrictions

The restricted definition of MT-safety for the iostream library means that a number of programming idioms used with iostream are unsafe in a multithreaded environment using shared iostream objects.

Checking Error State

To be MT-safe, error checking must occur in a critical region with the I/O operation that causes the error. The following example illustrates how to check for errors:


CODE EXAMPLE 11-1 Checking Error State
#include <iostream.h>
enum iostate {IOok, IOeof, IOfail};
 
iostate read_number(istream& istr, int& num)
{
	stream_locker sl(istr, stream_locker::lock_now);
 
	istr >> num;
 
	if (istr.eof()) return IOeof;
	if (istr.fail()) return IOfail;
	return IOok;
}

In this example, the constructor of the stream_locker object sl locks the istream object istr. The destructor of sl, called at the termination of read_number, unlocks istr.

Obtaining Characters Extracted by Last Unformatted Input Operation

To be MT-safe, the gcount function must be called within a thread that has exclusive use of the istream object for the period that includes the execution of the last input operation and gcount call. The following example shows a call to gcount:


CODE EXAMPLE 11-2 Calling gcount
#include <iostream.h>
#include <rlocks.h>
void fetch_line(istream& istr, char* line, int& linecount)
{
	stream_locker sl(istr, stream_locker::lock_defer);
 
	sl.lock(); // lock the stream istr
	istr >> line;
	linecount = istr.gcount();
	sl.unlock(); // unlock istr
	...
}

In this example, the lock and unlock member functions of class stream_locker define a mutual exclusion region in the program.

User-Defined I/O Operations

To be MT-safe, I/O operations defined for a user-defined type that involve a specific ordering of separate operations must be locked to define a critical region. The following example shows a user-defined I/O operation:


CODE EXAMPLE 11-3 User-Defined I/O Operations
#include <rlocks.h>
#include <iostream.h>
class mystream: public istream {
 
	// other definitions...
	int getRecord(char* name, int& id, float& gpa);
};
 
int mystream::getRecord(char* name, int& id, float& gpa)
{
	stream_locker sl(this, stream_locker::lock_now);
 
	*this >> name;
	*this >> id;
	*this >> gpa;
 
	return this->fail() == 0;
}

11.4.1.4 Reducing Performance Overhead of MT-Safe Classes

Using the MT-safe classes in this version of the libC library results in some amount of performance overhead, even in a single-threaded application; however, if you use the unsafe_ classes of libC, this overhead can be avoided.

The scope resolution operator can be used to execute member functions of the base unsafe_ classes; for example:


	cout.unsafe_ostream::put('4');

	cin.unsafe_istream::read(buf, len);

 

Note - The unsafe_ classes cannot be safely used in multithreaded applications.



Instead of using unsafe_ classes, you can make the cout and cin objects unsafe and then use the normal operations. A slight performance deterioration results. The following example shows how to use unsafe cout and cin:


CODE EXAMPLE 11-4 Disabling MT-Safety
#include <iostream.h>
//disable mt-safety
cout.set_safe_flag(stream_MT::unsafe_object);	
//disable mt-safety
cin.set_safe_flag(stream_MT::unsafe_object);	
cout.put(`4');
cin.read(buf, len);

When an iostream object is MT-safe, mutex locking is provided to protect the object's member variables. This locking adds unnecessary overhead to an application that only executes in a single-threaded environment. To improve performance, you can dynamically switch an iostream object to and from MT-safety. The following example makes an iostream object MT-unsafe:


CODE EXAMPLE 11-5 Switching to MT-Unsafe
fs.set_safe_flag(stream_MT::unsafe_object);// disable MT-safety
	.... do various i/o operations

You can safely use an MT-unsafe stream in code where an iostream is not shared by threads; for example, in a program that has only one thread, or in a program where each iostream is private to a thread.

If you explicitly insert synchronization into the program, you can also safely use MT-unsafe iostreams in an environment where an iostream is shared by threads. The following example illustrates the technique:


CODE EXAMPLE 11-6 Using Synchronization With MT-Unsafe Objects
	generic_lock(); 
	fs.set_safe_flag(stream_MT::unsafe_object);
	... do various i/o operations
	generic_unlock();

where the generic_lock and generic_unlock functions can be any synchronization mechanism that uses such primitives as mutex, semaphores, or reader/writer locks.



Note - The stream_locker class provided by the libC library is the preferred mechanism for this purpose.



See Section 11.4.5, Object Locks for more information.

11.4.2 Interface Changes to the iostream Library

This section describes the interface changes made to the iostream library to make it MT-Safe.

11.4.2.1 The New Classes

The following table lists the new classes added to the libC interfaces.


CODE EXAMPLE 11-7 New Classes
	stream_MT
	stream_locker
	unsafe_ios
	unsafe_istream
	unsafe_ostream
	unsafe_iostream
	unsafe_fstreambase
	unsafe_strstreambase

11.4.2.2 The New Class Hierarchy

The following table lists the new class hierarchy added to the iostream interfaces.


CODE EXAMPLE 11-8 New Class Hierarchy
class streambuf: public stream_MT {...};
class unsafe_ios {...};
class ios: virtual public unsafe_ios, public stream_MT {...};
class unsafe_fstreambase: virtual public unsafe_ios {...};
class fstreambase: virtual public ios, public unsafe_fstreambase 
  {...};
class unsafe_strstreambase: virtual public unsafe_ios {...};
class strstreambase: virtual public ios, public unsafe_strstreambase {...};
class unsafe_istream: virtual public unsafe_ios {...};
class unsafe_ostream: virtual public unsafe_ios {...};
class istream: virtual public ios, public unsafe_istream {...};
class ostream: virtual public ios, public unsafe_ostream {...};
class unsafe_iostream: public unsafe_istream, public unsafe_ostream {...};

11.4.2.3 The New Functions

The following table lists the new functions added to the iostream interfaces.


CODE EXAMPLE 11-9 New Functions
 class streambuf {
 public:
   int sgetc_unlocked();				
   void sgetn_unlocked(char *, int);
   int snextc_unlocked();
   int sbumpc_unlocked();
   void stossc_unlocked();
   int in_avail_unlocked();
   int sputbackc_unlocked(char);
   int sputc_unlocked(int);
   int sputn_unlocked(const char *, int);
   int out_waiting_unlocked();
 protected:
   char* base_unlocked();
   char* ebuf_unlocked();
   int blen_unlocked();
   char* pbase_unlocked();
   char* eback_unlocked();
   char* gptr_unlocked();
   char* egptr_unlocked();
   char* pptr_unlocked();
   void setp_unlocked(char*, char*);
   void setg_unlocked(char*, char*, char*);
   void pbump_unlocked(int);
   void gbump_unlocked(int);
   void setb_unlocked(char*, char*, int);
   int unbuffered_unlocked();
   char *epptr_unlocked();
   void unbuffered_unlocked(int);
   int allocate_unlocked(int);
 };
 
 class filebuf: public streambuf {
 public:
  int is_open_unlocked();
  filebuf* close_unlocked();
  filebuf* open_unlocked(const char*, int, int = 
    filebuf::openprot);
  
  filebuf* attach_unlocked(int);
 };
 
 class strstreambuf: public streambuf {
 public:
  int freeze_unlocked();
  char* str_unlocked();
 };
 
  
 unsafe_ostream& endl(unsafe_ostream&);
 unsafe_ostream& ends(unsafe_ostream&);
 unsafe_ostream& flush(unsafe_ostream&);
 unsafe_istream& ws(unsafe_istream&);
 unsafe_ios& dec(unsafe_ios&);
 unsafe_ios& hex(unsafe_ios&);
 unsafe_ios& oct(unsafe_ios&);
 
 char* dec_r (char* buf, int buflen, long num, int width)
 char* hex_r (char* buf, int buflen, long num, int width)
 char* oct_r (char* buf, int buflen, long num, int width) 
 char* chr_r (char* buf, int buflen, long chr, int width)
 char* str_r (char* buf, int buflen, const char* format, int width 
    = 0);
 char* form_r (char* buf, int buflen, const char* format,...) 

11.4.3 Global and Static Data

Global and static data in a multithreaded application are not safely shared among threads. Although threads execute independently, they share access to global and static objects within the process. If one thread modifies such a shared object, all the other threads within the process observe the change, making it difficult to maintain state over time. In C++, class objects (instances of a class) maintain state by the values in their member variables. If a class object is shared, it is vulnerable to changes made by other threads.

When a multithreaded application uses the iostream library and includes iostream.h, the standard streams--cout, cin, cerr, and clog-- are, by default, defined as global shared objects. Since the iostream library is MT-safe, it protects the state of its shared objects from access or change by another thread while a member function of an iostream object is executing. However, the scope of MT-safety for an object is confined to the period in which the object's public member function is executing. For example,


	int c;
	cin.get(c);

gets the next character in the get buffer and updates the buffer pointer in ThreadA. However, if the next instruction in ThreadA is another get call, the libC library does not guarantee to return the next character in the sequence. It is not guaranteed because, for example, ThreadB may have also executed the get call in the intervening period between the two get calls made in ThreadA.

See Section 11.4.5, Object Locks for strategies for dealing with the problems of shared objects and multithreading.

11.4.4 Sequence Execution

Frequently, when iostream objects are used, a sequence of I/O operations must be MT-safe. For example, the code:


cout << " Error message:" << errstring[err_number] << "\n";

involves the execution of three member functions of the cout stream object. Since cout is a shared object, the sequence must be executed atomically as a critical section to work correctly in a multithreaded environment. To perform a sequence of operations on an iostream class object atomically, you must use some form of locking.

The libC library now provides the stream_locker class for locking operations on an iostream object. See Section 11.4.5, Object Locks for information about the stream_locker class.

11.4.5 Object Locks

The simplest strategy for dealing with the problems of shared objects and multithreading is to avoid the issue by ensuring that iostream objects are local to a thread. For example,

However, in many cases, such as default shared standard stream objects, it is not possible to make the objects local to a thread, and an alternative strategy is required.

To perform a sequence of operations on an iostream class object atomically, you must use some form of locking. Locking adds some overhead even to a single-threaded application. The decision whether to add locking or make iostream objects private to a thread depends on the thread model chosen for the application: Are the threads to be independent or cooperating?

11.4.5.1 Class stream_locker

The iostream library provides the stream_locker class for locking a series of operations on an iostream object. You can, therefore, minimize the performance overhead incurred by dynamically enabling or disabling locking in iostream objects.

Objects of class stream_locker can be used to make a sequence of operations on a stream object atomic. For example, the code shown in the example below seeks to find a position in a file and reads the next block of data.


CODE EXAMPLE 11-10 Example of Using Locking Operations
#include <fstream.h>
#include <rlocks.h>
 
void lock_example (fstream& fs)
{
    const int len = 128;
    char buf[len];
    int offset = 48;
	stream_locker s_lock(fs, stream_locker::lock_now);
	.....// open file
	fs.seekg(offset, ios::beg);
	fs.read(buf, len);
}

In this example, the constructor for the stream_locker object defines the beginning of a mutual exclusion region in which only one thread can execute at a time. The destructor, called after the return from the function, defines the end of the mutual exclusion region. The stream_locker object ensures that both the seek to a particular offset in a file and the read from the file are performed together, atomically, and that ThreadB cannot change the file offset before the original ThreadA reads the file.

An alternative way to use a stream_locker object is to explicitly define the mutual exclusion region. In the following example, to make the I/O operation and subsequent error checking atomic, lock and unlock member function calls of a vbstream_locker object are used.


CODE EXAMPLE 11-11 Making I/O Operation and Error Checking Atomic
{
	...
	stream_locker file_lck(openfile_stream,
	                         stream_locker::lock_defer);
	....
	file_lck.lock();  // lock openfile_stream
	openfile_stream << "Value: " << int_value << "\n";
	if(!openfile_stream) {
			file_error("Output of value failed\n");
			return;
	}
	file_lck.unlock(); // unlock openfile_stream
}

For more information, see the stream_locker(3CC4) man page.

11.4.6 MT-Safe Classes

You can extend or specialize the functionality of the iostream classes by deriving new classes. If objects instantiated from the derived classes will be used in a multithreaded environment, the classes must be MT-safe.

Considerations when deriving MT-safe classes include:

11.4.7 Object Destruction

Before an iostream object that is shared by several threads is deleted, the main thread must verify that the subthreads are finished with the shared object. The following example shows how to safely destroy a shared object.


CODE EXAMPLE 11-12 Destroying a Shared Object
#include <fstream.h>
#include <thread.h>
fstream* fp;
 
void *process_rtn(void*)
{
	// body of sub-threads which uses fp...
}
 
void multi_process(const char* filename, int numthreads) 
{
	fp = new fstream(filename, ios::in); // create fstream object
                                         // before creating threads.
	// create threads
	for (int i=0; i<numthreads; i++)
			thr_create(0, STACKSIZE, process_rtn, 0, 0, 0);
 
		...
	// wait for threads to finish
	for (int i=0; i<numthreads; i++)
			thr_join(0, 0, 0);
 
	delete fp;                          // delete fstream object after
	fp = NULL;                         // all threads have completed.
}

11.4.8 An Example Application

The following code provides an example of a multiply-threaded application that uses iostream objects from the libC library in an MT-safe way.

The example application creates up to 255 threads. Each thread reads a different input file, one line at a time, and outputs the line to an output file, using the standard output stream, cout. The output file, which is shared by all threads, is tagged with a value that indicates which thread performed the output operation.


CODE EXAMPLE 11-13 Using iostream Objects in an MT-Safe Way
// create tagged thread data
// the output file is of the form:
//	 	<tag><string of data>\n
// where tag is an integer value in a unsigned char. 
// Allows up to 255 threads to be run in this application
// <string of data> is any printable characters
// Because tag is an integer value written as char,
// you need to use od to look at the output file, suggest:
//			od -c out.file |more
 
#include <stdlib.h>
#include <stdio.h>
#include <iostream.h>
#include <fstream.h>
#include <thread.h>
 
struct thread_args {
  char* filename;
  int thread_tag;
};
 
const int thread_bufsize = 256;
 
// entry routine for each thread
void* ThreadDuties(void* v) {
// obtain arguments for this thread
  thread_args* tt = (thread_args*)v;
  char ibuf[thread_bufsize];
  // open thread input file
  ifstream instr(tt->filename);
  stream_locker lockout(cout, stream_locker::lock_defer);
  while(1) {
  // read a line at a time
    instr.getline(ibuf, thread_bufsize - 1, '\n');
    if(instr.eof())
      break;
  // lock cout stream so the i/o operation is atomic
    lockout.lock(); 
  // tag line and send to cout
    cout << (unsigned char)tt->thread_tag << ibuf << "\n";
    lockout.unlock();
  }
  return 0;
}
 
int main(int argc, char** argv) {
  // argv: 1+ list of filenames per thread
   if(argc < 2) {
     cout << "usage: " << argv[0] << " <files..>\n";
     exit(1);
   }
  int num_threads = argc - 1;
  int total_tags = 0;
 
// array of thread_ids
  thread_t created_threads[thread_bufsize];
// array of arguments to thread entry routine
  thread_args thr_args[thread_bufsize];
  int i;
  for(i = 0; i < num_threads; i++) {
    thr_args[i].filename = argv[1 + i];
// assign a tag to a thread - a value less than 256
    thr_args[i].thread_tag = total_tags++;
// create threads
    thr_create(0, 0, ThreadDuties, &thr_args[i], 
	       THR_SUSPENDED, &created_threads[i]);
  }
 
  for(i = 0; i < num_threads; i++) {
    thr_continue(created_threads[i]);
  }
  for(i = 0; i < num_threads; i++) {
    thr_join(created_threads[i], 0, 0);
  }
  return 0;
}