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 basic_streambuf, 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 basic_{i,o}stream<>::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 std::basic_istream<>::seekg() and std::basic_ostream<>::seekp().
Consider the following example:
Thread 1: | Thread 2: |
std::cout << "Hello Thread 1" << std::endl; |
std::cout << "Hello Thread 2" << std::endl; |
If Thread 1 is the first thread locking the buffer, the sequence of characters "Hello Thread 1" is output to the standard output and the lock is released; Thread 2 then acquires the lock, outputs its sequence of characters, and releases the lock.
Note that each of the statements performs two insertions: first the character string is inserted, then the manipulator. This involves two separate calls to the insertion operator, each with its own sentry object. There is a window of opportunity between the two insertions in which Thread 1 may be preempted and Thread 2 be given a chance to run. In other words, the output is not atomic with respect to the string and the terminating new line.
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. However, since the sentry classes nested in basic_istream and basic_ostream are responsible for the locking and unlocking of the stream buffer mutex, code that accesses stream buffers in a way that requires protection from multiple threads should first construct a sentry object to guarantee thread-safe exception. The following example illustrates how sentry objects work:
Thread 1: | Thread 2: |
std::cout << "Thread 1" ; |
const char *s = "Thread 2"; std::ios::sentry opfx(std::cout); while(*s) std::cout.rdbuf()->sputc(*s++); |
In this scheme, if Thread 2 is the first one to execute, when it constructs the sentry object, it locks the basic_streambuf object pointed at by std::cout.rdbuf(). Thread 1 also constructs a sentry object in the insertion operator; the sentry constructor must wait until Thread 2 reaches the end of the scope of the opfx sentry object. When opfx goes out of scope, the sentry destructor releases the lock. This technique is easy to use and allows high performance for both stream and buffer operations. It is also safe in the presence of exceptions, since if an exception occurs at any point after the construction of the sentry object the stack is unwound, the sentry destructor is called, and the mutex object is guaranteed to be released.