Antonio Perez
Antonio Perez

Reputation: 493

Thread safety and piping to streams

If two different threads each try writing one piece of data each to a output stream, is it guaranteed that this operation is thread-safe for std::string_stream, std::cout, std::err, and std::fstream (in C++11 onward)? (In other words, are there any guarantees that writing to the same stream from multiple threads won't break the stream or interleave single pieces of data?)

By "one piece of data", I mean "with one call to the pipe operator << using a standard library overload". For example, if one thread writes a string containing a million 0s in a row, and the other thread writes a string containing a million 1s in a row, are there any guarantees that there won't be any 0s mixed with 1s?

Code testing this idea: This code outputs a bunch of 1s on one thread, and a bunch of 0s in another thread. I haven't seen any interlacing when piping the output to a file. I tested this code out in Ubuntu 18.04, using gcc-7.3.0 and compiling with the flags -std=c++17 -pthread -O3 main.cc -o main, and I didn't observe any mixing of 1s and 0s.

#include <iostream>
#include <sstream>
#include <string>
#include <thread>
#include <chrono>

auto now() { 
    return std::chrono::high_resolution_clock::now();
}
typedef decltype(now()) now_time_t;
int main(int argc, char** argv) {
    std::stringstream A {}, B {};
    int count = argc > 1 ? std::stoi(argv[1]) : 1000;
    for(int i = 0; i < count; ++i) {
        A << "0";
        B << "1";
    }
    volatile bool waiting = true;
    now_time_t t1_start_time, t2_start_time, t1_end_time, t2_end_time;
    std::thread t1 = std::thread([&] { 
        while(waiting) ;
        t1_start_time = now();
        std::cout << A.rdbuf(); 
        t1_end_time = now();
    });
    std::thread t2 = std::thread([&] { 
        while(waiting) ;
        t2_start_time = now();
        std::cout << B.rdbuf(); 
        t2_end_time = now();
    });
    waiting = false;
    t1.join();
    t2.join();
    auto t1_total_time = (t1_end_time - t1_start_time).count();
    auto t2_total_time = (t2_end_time - t2_start_time).count();
    std::cerr << "Time difference: " << (t1_start_time - t2_start_time).count() << std::endl;    
    std::cerr << "t1 total time: " << t1_total_time << std::endl;    
    std::cerr << "t2 total time: " << t2_total_time << std::endl;    
}

Upvotes: 1

Views: 1045

Answers (1)

Igor Tandetnik
Igor Tandetnik

Reputation: 52471

[iostreams.threadsafety]/1 Concurrent access to a stream object (30.8, 30.9), stream buffer object (30.6), or C Library stream (30.11) by multiple threads may result in a data race (4.7) unless otherwise specified (30.4). [ Note: Data races result in undefined behavior (4.7). —end note ]

[iostream.objects.overview]/5 Concurrent access to a synchronized (30.5.3.4) standard iostream object’s formatted and unformatted input (30.7.4.1) and output (30.7.5.1) functions or a standard C stream by multiple threads shall not result in a data race (4.7). [ Note: Users must still synchronize concurrent use of these objects and streams by multiple threads if they wish to avoid interleaved characters. —end note ]

Here "standard iostream object" is one of std::cin, std::cout, std::cerr, std::clog and the corresponding wide streams (wcin et al). Such a stream is synchronized unless sync_with_stdio(false) has been previously called on it.

Thus, concurrent writes to std::cout et al don't result in a data race, but are allowed to interleave characters. Concurrent writes to any other stream (e.g. string stream, file stream) exhibit undefined behavior by way of a data race.

Upvotes: 2

Related Questions