ISO/IEC JTC1 SC22 WG21 N3535 - 2013-03-06
Lawrence Crowl, crowl@google.com, Lawrence@Crowl.org
Introduction
Solution
Stream Mutex
Implicit Locking
Explicit Locking
Block Locking
Using Normal Locking
Recursive Locking
Critique and Suggestions
Posix Stream Locks
Internal Stream Locks
Replace Streams
Summary
Implementation
Synopsis
Stream Mutex
Stream Guard
Stream Operators
Predefined Objects
Revision History
References
At present, stream output operations guarantee that they will not produce race conditions, but do not guarantee that the effect will be sensible. Some form of external synchronization is required. Unfortunately, without a standard mechanism for synchronizing, independently developed software will be unable to synchronize.
This paper proposes a standard mechanism for locking streams.
We found the proposed mechanism extremely helpful in making sense of debugging logs in multi-thread tests. When numbers are randomly streamed together, teasing out which digit went with which number can be very frustrating.
First, we propose a stream_mutex
,
analogous to mutex
,
that serves as a mutex for a stream.
Unlike normal mutexes, though,
stream_mutex
is a proxy for an anonymous mutex
for each distinct stream used as a constructor parameter.
Second, we propose a stream_guard
,
analogous to lock_guard
,
that provides scoped lock control.
These classes are distinct from existing classes
to support streaming through the mutex/lock objects,
which substantially simplifies locking I/O expressions.
A stream mutex is declared with a reference to the stream to be locked. Each stream to be locked may have more than one simultaneous associated stream mutex object. Two independent threads creating two stream mutex objects on the same stream will mutually exclude operations on the stream.
std::ostringstream stream;
stream_mutex<std::ostream> mstream(stream);
Stream operations may simply be directed to the stream mutex. All operations in a single expression chain will be locked as a unit. The lock will be released at the end of the full expression.
mstream << "1" << "2" << "3" << "4" << "5" << std::endl;
The locking may be made explicit with the hold
member function.
The lock will be released at the end of the full expression.
mstream.hold() << "1" << "2" << "3" << "4" << "5" << std::endl;
Locking across more than one expression is needed, and the stream guard serves that purpose. The lock is released on destruction of the guard.
{
stream_guard<std::ostream> gstream(mstream);
gstream << "1";
gstream << "2";
gstream << "3";
gstream << "4";
gstream << "5";
gstream << std::endl;
}
While stream_guard
is convenient,
still the normal locking facilities must work.
Hence, stream_mutex
supports the mutex interface.
To provide non-locking access through the stream_mutex
,
it provides a bypass
operation.
{
lock_guard<stream_mutex<std::ostream> > lck(mstream);
std::ostream& rawstream = mstream.bypass();
rawstream << "1";
rawstream << "2";
rawstream << "3";
rawstream << "4";
rawstream << "5";
rawstream << std::endl;
}
{
unique_lock<stream_mutex<std::ostream> > lck(mstream, defer_lock);
lck.lock();
std::ostream& rawstream = mstream.bypass();
rawstream << "1";
rawstream << "2";
rawstream << "3";
rawstream << "4";
rawstream << "5";
rawstream << std::endl;
}
{
unique_lock<stream_mutex<std::ostream> > lck(mstream, defer_lock);
if ( lck.try_lock() ) {
std::ostream& rawstream = mstream.bypass();
rawstream << "1";
rawstream << "2";
rawstream << "3";
rawstream << "4";
rawstream << "5";
rawstream << std::endl;
}
}
Streams may be locked in uncoordinated code, and so recursive locking must work.
{
lock_guard<stream_mutex<std::ostream> > lck(mstream);
mstream.hold() << "1" << "2" << "3";
mstream << "4" << "5" << std::endl;
}
A consequence of this tolerance for recursion is that
the bypass
operation is a performance optimization.
There are three problems with the proposal above, and the current implementation.
The stream_mutex
object
must maintain a (static duration) mapping from streams to mutexes.
The constructor of a stream mutex
will look for the stream address in the map.
If the address is present, it will use the associated mutex.
Otherwise, the constructor will insert an entry.
The locking operations on the stream_mutex
will be forwarded to the appropriate entry.
Because accesses to this map will be concurrent, they must be protected against races. In the simple case, the number of lock operations could double.
To prevent resource leakage in programs that create and destroy many streams, the stream mutex would likely also need to maintain a reference count on each entry. The reference count is incremented on stream mutex construction, and decremented on stream mutex destruction. An initial implementation need not go this far, though, as unbounded creation of streams is unusual.
One suggestion to avoid this problem was that the implementation should use a Posix stream lock [PSL].
#include <stdio.h> void flockfile(FILE *file); int ftrylockfile(FILE *file); void funlockfile(FILE *file);
However, that solution is not generally available.
FILE*
from the iostream
,
so the stream_mutex
implementation cannot get to it.
iostream
s can and do open files
without using FILE*
.
The Posix facility is thus inapplicable.
iostream
facilies enable streams without corresponding files.
Again, the Posix facility is thus inapplicable.
Another suggestion is to require
streambuf
to export its own locking interface,
which would then be propogated through the other types.
There are at least two problems here.
streambuf
for the mutex
will change the class size and hence the ABI.
One suggestion has been to replace streams with something that does not have streams known weaknesses. While this approach has merit for future code, it does not help existing code that is committed to streams.
In summary,
the proposed stream_mutex
mechanism is not ideal.
In the absence of ABI considerations,
putting a mutex into the streambuf itself is ideal.
However, ABI considerations are very important.
Achieving that solution may not happen,
and if it does, may take considerable time.
The proposed mapping approach is workable,
though less efficient than desireable.
Therefore, we present this proposal for C++14.
The open-source implementation is freely available at http://code.google.com/p/google-concurrency-library/source/browse/include/stream_mutex.h. The current implementation does not reference count the stream to mutex mapping.
template <class Stream >
class stream_mutex
{
public:
constexpr stream_mutex(Stream& stm);
~stream_mutex();
void lock();
void unlock();
bool try_lock();
stream_guard<Stream> hold();
Stream& bypass();
};
template <class Stream>
class stream_guard
{
public:
stream_guard(stream_mutex<Stream>& mtx);
~stream_guard();
Stream& bypass() const;
};
template <typename Stream, typename T>
const stream_guard<Stream>&
operator<<(const stream_guard<Stream>& lck, T arg);
template <typename Stream>
const stream_guard<Stream>&
operator<<(const stream_guard<Stream>& lck, Stream& (*arg)(Stream&));
template <typename Stream, typename T>
const stream_guard<Stream>&
operator>>(const stream_guard<Stream>& lck, T& arg);
template <typename Stream, typename T>
stream_guard<Stream>
operator<<(stream_mutex<Stream>& mtx, T arg);
template <typename Stream>
stream_guard<Stream>
operator<<(stream_mutex<Stream>& mtx, Stream& (*arg)(Stream&));
template <typename Stream, typename T>
stream_guard<Stream>
operator>>(stream_mutex<Stream>& mtx, T& arg);
extern stream_mutex<istream> mcin;
extern stream_mutex<ostream> mcout;
extern stream_mutex<ostream> mcerr;
extern stream_mutex<ostream> mclog;
extern stream_mutex<wistream> mwcin;
extern stream_mutex<wostream> mwcout;
extern stream_mutex<wostream> mwcerr;
extern stream_mutex<wostream> mwclog;
This paper revises N3395 = 12-0085 - 2012-09-23 as follows.
Choose the approach of maintaining a stream to mutex mapping, which allows multiple stream_mutex objects to manage a single stream.
Add an elaboration of the consequences of the above decision to the 'Critique and Suggestions' section.
Add a discussion of replacing streams to the 'Critique and Suggestions' section.
Add a 'Revision History' section.
N3395 revised N3354 = 12-0044 - 2012-01-14 as follows.
Clarify usefulness of stream mutexes in debugging, testing, and diagnostics.
Apply common subexpression elimination
on calls to bypass()
in the examples.
Add a 'Critique and Suggestions' section discussing the topics of Posix stream locks, internal stream locks, and maintaining a stream to mutex mapping.
Add an 'Implementation' section.
Add a section on 'Predefined Objects' corresponding to the standard streams.
Add a 'References' section.