Skip to content

Commit 7472d37

Browse files
authored
Adding iostream.h thread-safety documentation. (#2995)
* Adding iostream.h thread-safety documentation. * Restoring `TestThread` code with added `std::lock_guard<std::mutex>`. * Updating new comments to reflect new information. * Fixing up `git rebase -X theirs` accidents.
1 parent 2d46869 commit 7472d37

File tree

3 files changed

+49
-21
lines changed

3 files changed

+49
-21
lines changed

docs/advanced/pycpp/utilities.rst

+11
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ redirects output to the corresponding Python streams:
4747
call_noisy_func();
4848
});
4949
50+
.. warning::
51+
52+
The implementation in ``pybind11/iostream.h`` is NOT thread safe. Multiple
53+
threads writing to a redirected ostream concurrently cause data races
54+
and potentially buffer overflows. Therefore it is currrently a requirement
55+
that all (possibly) concurrent redirected ostream writes are protected by
56+
a mutex. #HelpAppreciated: Work on iostream.h thread safety. For more
57+
background see the discussions under
58+
`PR #2982 <https://github.com/pybind/pybind11/pull/2982>`_ and
59+
`PR #2995 <https://github.com/pybind/pybind11/pull/2995>`_.
60+
5061
This method respects flushes on the output streams and will flush if needed
5162
when the scoped guard is destroyed. This allows the output to be redirected in
5263
real time, such as to a Jupyter notebook. The two arguments, the C++ stream and

include/pybind11/iostream.h

+25-20
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
66
All rights reserved. Use of this source code is governed by a
77
BSD-style license that can be found in the LICENSE file.
8+
9+
WARNING: The implementation in this file is NOT thread safe. Multiple
10+
threads writing to a redirected ostream concurrently cause data races
11+
and potentially buffer overflows. Therefore it is currrently a requirement
12+
that all (possibly) concurrent redirected ostream writes are protected by
13+
a mutex.
14+
#HelpAppreciated: Work on iostream.h thread safety.
15+
For more background see the discussions under
16+
https://github.com/pybind/pybind11/pull/2982 and
17+
https://github.com/pybind/pybind11/pull/2995.
818
*/
919

1020
#pragma once
@@ -85,30 +95,25 @@ class pythonbuf : public std::streambuf {
8595
return remainder;
8696
}
8797

88-
// This function must be non-virtual to be called in a destructor. If the
89-
// rare MSVC test failure shows up with this version, then this should be
90-
// simplified to a fully qualified call.
98+
// This function must be non-virtual to be called in a destructor.
9199
int _sync() {
92100
if (pbase() != pptr()) { // If buffer is not empty
93101
gil_scoped_acquire tmp;
94-
// Placed inside gil_scoped_acquire as a mutex to avoid a race.
95-
if (pbase() != pptr()) { // Check again under the lock
96-
// This subtraction cannot be negative, so dropping the sign.
97-
auto size = static_cast<size_t>(pptr() - pbase());
98-
size_t remainder = utf8_remainder();
99-
100-
if (size > remainder) {
101-
str line(pbase(), size - remainder);
102-
pywrite(line);
103-
pyflush();
104-
}
105-
106-
// Copy the remainder at the end of the buffer to the beginning:
107-
if (remainder > 0)
108-
std::memmove(pbase(), pptr() - remainder, remainder);
109-
setp(pbase(), epptr());
110-
pbump(static_cast<int>(remainder));
102+
// This subtraction cannot be negative, so dropping the sign.
103+
auto size = static_cast<size_t>(pptr() - pbase());
104+
size_t remainder = utf8_remainder();
105+
106+
if (size > remainder) {
107+
str line(pbase(), size - remainder);
108+
pywrite(line);
109+
pyflush();
111110
}
111+
112+
// Copy the remainder at the end of the buffer to the beginning:
113+
if (remainder > 0)
114+
std::memmove(pbase(), pptr() - remainder, remainder);
115+
setp(pbase(), epptr());
116+
pbump(static_cast<int>(remainder));
112117
}
113118
return 0;
114119
}

tests/test_iostream.cpp

+13-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
#include "pybind11_tests.h"
1616
#include <atomic>
1717
#include <iostream>
18+
#include <mutex>
19+
#include <string>
1820
#include <thread>
1921

2022
void noisy_function(const std::string &msg, bool flush) {
@@ -35,8 +37,18 @@ void noisy_funct_dual(const std::string &msg, const std::string &emsg) {
3537
struct TestThread {
3638
TestThread() : stop_{false} {
3739
auto thread_f = [this] {
40+
static std::mutex cout_mutex;
3841
while (!stop_) {
39-
std::cout << "x" << std::flush;
42+
{
43+
// #HelpAppreciated: Work on iostream.h thread safety.
44+
// Without this lock, the clang ThreadSanitizer (tsan) reliably reports a
45+
// data race, and this test is predictably flakey on Windows.
46+
// For more background see the discussion under
47+
// https://github.com/pybind/pybind11/pull/2982 and
48+
// https://github.com/pybind/pybind11/pull/2995.
49+
const std::lock_guard<std::mutex> lock(cout_mutex);
50+
std::cout << "x" << std::flush;
51+
}
4052
std::this_thread::sleep_for(std::chrono::microseconds(50));
4153
} };
4254
t_ = new std::thread(std::move(thread_f));

0 commit comments

Comments
 (0)