C++ Stream Mutexes

ISO/IEC JTC1 SC22 WG21 N3354 = 12-0044 - 2012-01-14

Lawrence Crowl, crowl@google.com, Lawrence@Crowl.org

Introduction
Solution
    Stream Mutex
    Implicit Locking
    Explicit Locking
    Block Locking
    Using Normal Locking
    Recursive Locking
Synopsis
    Stream Mutex
    Stream Guard
    Stream Operators

Introduction

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.

Solution

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.

We have found these classes essential in testing and diagnostic reporting within other concurrent code.

Stream Mutex

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);

Implicit Locking

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;

Explicit Locking

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;

Block Locking

Locking across more than one expression is needed, and the stream guard serves that purpose.

{
stream_guard<std::ostream> gstream(mstream);
gstream << "1";
gstream << "2";
gstream << "3";
gstream << "4";
gstream << "5";
gstream << std::endl;
}

Using Normal Locking

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);
mstream.bypass() << "1";
mstream.bypass() << "2";
mstream.bypass() << "3";
mstream.bypass() << "4";
mstream.bypass() << "5";
mstream.bypass() << std::endl;
}

{
unique_lock<stream_mutex<std::ostream> > lck(mstream, defer_lock);
lck.lock();
mstream.bypass() << "1";
mstream.bypass() << "2";
mstream.bypass() << "3";
mstream.bypass() << "4";
mstream.bypass() << "5";
mstream.bypass() << std::endl;
}

{
unique_lock<stream_mutex<std::ostream> > lck(mstream, defer_lock);
if ( lck.try_lock() ) {
    mstream.bypass() << "1";
    mstream.bypass() << "2";
    mstream.bypass() << "3";
    mstream.bypass() << "4";
    mstream.bypass() << "5";
    mstream.bypass() << std::endl;
}
}

Recursive Locking

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.

Synopsis

Stream Mutex

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();
};

Stream Guard

template <class Stream>
class stream_guard
{
  public:
    stream_guard(stream_mutex<Stream>& mtx);
    ~stream_guard();
    Stream& bypass() const;
};

Stream Operators

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);