The locking mechanism has been designed for maximum efficiency, with no reentrant locks needed. There are two different mutex objects involved in this scheme:
The first mutex object is located in the class ios_base. It enforces multithread safety for all formatting operations performed on the stream, for imbuing the stream with a new locale object, and for accessing the raw storage mechanism (pword, iword). All functions performing these operations lock the mutex object beforehand and release it afterwards. These operations are not time-critical and do not occur often in comparison to buffer operations like inserting a character. They are located in classes ios_base or basic_ios.
The second mutex object, located in the basic_streambuf class, protects the buffer. The locking and unlocking of this mutex object is critical, since buffer operations are on the direct path of performance issues.
It is easy to see that locking and unlocking the buffer after each independent buffer operation would be disastrous. For example, when inserting a char* sequence of characters, a call to an inline basic_streambuf function is made for each character inserted; therefore, the locking mechanism is carried out at a higher level. For all formatted and unformatted stream functions, the locking is performed in the sentry object constructor, and the release in the sentry object destructor. If the function does not make use of the sentry class, the lock is directly performed inside the function. This is the case with basic_istream::seekg and basic_ostream::seekg.
Consider the following example:
Thread 1: |
Thread 2: |
cout << "Hello Thread 1" << endl; |
cout << "Hello Thread 2" << endl; |
If Thread 1 is the first thread locking the buffer, the sequence of characters "Hello Thread 1" is output to stdout and the lock is released; Thread 2 then acquires the lock, outputs its sequence of characters, and releases it.
Notice that only one lock occurs on the basic_streambuf mutex object for each stream operation. The advantage of this scheme is obviously high performance, but the drawback is that while buffer functionality is directly accessed, the buffer is left unprotected. We therefore provide functions and manipulators to solve this problem. The following example explains how they work:
Thread 1: |
Thread 2: |
cout << "Hello Thread 1" << endl; |
cout<< __lock; do { cout.rdbuf()->sputc(*t); } while ( *t++!=0 ) cout << __unlock; |
In this scheme, if Thread 2 is the first one to execute, when it gets to the statement cout<< __lock;, it locks the basic_streambuf object pointed at by cout.rdbuf(). Thread 1 cannot output its sequence of characters until Thread 2 reaches the statement cout << __unlock;, which releases the lock. This technique is easy to use and allows high performance for both stream and buffer operations.
There is also a way to lock several stream operations within one thread in order to preserve the global order of operations carried out by several threads running concurrently. The following example illustrates this technique:
Thread 1: |
Thread 2: |
cout << __lock; cout << "Thread 1 begin" << endl; cout << "Thread 1 completion" << endl; cout << __unlock; |
cout << "Thread 2 begin" << endl; |
In this example, if Thread 1 is the first thread locking the basic_streambuf object attached to cout, Thread 2 cannot output its sequence of characters until Thread 1 reaches the statement cout << __unlock;. The result is to preserve the order of the output, which is:
Thread 1 locking first: |
Thread 1 begin Thread 1 completion Thread 2 begin |
Thread 2 locking first: |
Thread 2 begin Thread 1 begin Thread 1 completion |
OEM Edition, ©Copyright 1999, Rogue Wave Software, Inc.
Contact Rogue Wave about documentation or support issues.