ISO/IEC JTC1 SC22 WG21 N3395 = 12-0085 - 2012-09-23
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
Maintain a Stream to Mutex Mapping
Summary
Implementation
Synopsis
Stream Mutex
Stream Guard
Stream Operators
Predefined Objects
References
This paper revises N3354 = 12-0044 - 2012-01-14.
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.
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 needs exactly one stream mutex.
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.
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.
The primary problem of the proposal above,
and the current implementation,
is that the stream_mutex
object
is separate from the stream objects themselves.
This separation in objects requires
careful management.
Using two different stream_mutex
objects
to manage the same stream
object
will be ineffective.
This management should not be a burden for the standard-defined stream objects,
as they will each have standard-defined stream mutexs.
The problem only arises for user-created streams.
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.
Another suggestion is to make the stream mutex a proxy for a mutex in a static-duration map from stream to mutex. 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. Locking operations on the stream mutex will be forwarded to the appropriate entry.
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.
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.
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 mapping approach is workable,
though its efficiency is poor.
Meanwhile the original problem persists. Therefore, we present this proposal contingently, based on thorough discussion of the suggestions. In the meantime, or if there is no change, this proposal serves a tool programmers can use immediately.
The open-source implementation is freely available at http://code.google.com/p/google-concurrency-library/source/browse/include/stream_mutex.h.
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;