Skip to content

Commit f443f9f

Browse files
committed
Adding iostream.h thread-safety documentation.
1 parent 795e3c4 commit f443f9f

File tree

4 files changed

+35
-85
lines changed

4 files changed

+35
-85
lines changed

docs/advanced/pycpp/utilities.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ 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 a requirement that
55+
all (possibly) concurrent redirected ostream writes are locked. Note
56+
that this is not expected to be an actual limitation, because without
57+
synchronization output will be randomly interleaved and most likely
58+
unreadable. Well-written C++ code is likely to use locking regardless of
59+
this pybind11 requirement. — For more background see the discussion under
60+
`PR #2982 <https://github.com/pybind/pybind11/pull/2982>`_.
61+
5062
This method respects flushes on the output streams and will flush if needed
5163
when the scoped guard is destroyed. This allows the output to be redirected in
5264
real time, such as to a Jupyter notebook. The two arguments, the C++ stream and

include/pybind11/iostream.h

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
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 and
11+
potentially buffer overflows. Therefore it is a REQUIREMENT that all
12+
(possibly) concurrent redirected ostream writes are locked. For more
13+
background see the discussion under
14+
https://github.com/pybind/pybind11/pull/2982.
815
*/
916

1017
#pragma once
@@ -85,30 +92,25 @@ class pythonbuf : public std::streambuf {
8592
return remainder;
8693
}
8794

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.
95+
// This function must be non-virtual to be called in a destructor.
9196
int _sync() {
9297
if (pbase() != pptr()) { // If buffer is not empty
9398
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));
99+
// This subtraction cannot be negative, so dropping the sign.
100+
auto size = static_cast<size_t>(pptr() - pbase());
101+
size_t remainder = utf8_remainder();
102+
103+
if (size > remainder) {
104+
str line(pbase(), size - remainder);
105+
pywrite(line);
106+
pyflush();
111107
}
108+
109+
// Copy the remainder at the end of the buffer to the beginning:
110+
if (remainder > 0)
111+
std::memmove(pbase(), pptr() - remainder, remainder);
112+
setp(pbase(), epptr());
113+
pbump(static_cast<int>(remainder));
112114
}
113115
return 0;
114116
}

tests/test_iostream.cpp

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313

1414
#include <pybind11/iostream.h>
1515
#include "pybind11_tests.h"
16-
#include <atomic>
1716
#include <iostream>
18-
#include <thread>
17+
#include <string>
1918

2019
void noisy_function(const std::string &msg, bool flush) {
2120

@@ -29,40 +28,6 @@ void noisy_funct_dual(const std::string &msg, const std::string &emsg) {
2928
std::cerr << emsg;
3029
}
3130

32-
// object to manage C++ thread
33-
// simply repeatedly write to std::cerr until stopped
34-
// redirect is called at some point to test the safety of scoped_estream_redirect
35-
struct TestThread {
36-
TestThread() : stop_{false} {
37-
auto thread_f = [this] {
38-
while (!stop_) {
39-
std::cout << "x" << std::flush;
40-
std::this_thread::sleep_for(std::chrono::microseconds(50));
41-
} };
42-
t_ = new std::thread(std::move(thread_f));
43-
}
44-
45-
~TestThread() {
46-
delete t_;
47-
}
48-
49-
void stop() { stop_ = true; }
50-
51-
void join() const {
52-
py::gil_scoped_release gil_lock;
53-
t_->join();
54-
}
55-
56-
void sleep() {
57-
py::gil_scoped_release gil_lock;
58-
std::this_thread::sleep_for(std::chrono::milliseconds(50));
59-
}
60-
61-
std::thread *t_{nullptr};
62-
std::atomic<bool> stop_;
63-
};
64-
65-
6631
TEST_SUBMODULE(iostream, m) {
6732

6833
add_ostream_redirect(m);
@@ -104,10 +69,4 @@ TEST_SUBMODULE(iostream, m) {
10469
std::cout << msg << std::flush;
10570
std::cerr << emsg << std::flush;
10671
});
107-
108-
py::class_<TestThread>(m, "TestThread")
109-
.def(py::init<>())
110-
.def("stop", &TestThread::stop)
111-
.def("join", &TestThread::join)
112-
.def("sleep", &TestThread::sleep);
11372
}

tests/test_iostream.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -306,26 +306,3 @@ def test_redirect_both(capfd):
306306
assert stderr == ""
307307
assert stream.getvalue() == msg
308308
assert stream2.getvalue() == msg2
309-
310-
311-
def test_threading():
312-
with m.ostream_redirect(stdout=True, stderr=False):
313-
# start some threads
314-
threads = []
315-
316-
# start some threads
317-
for _j in range(20):
318-
threads.append(m.TestThread())
319-
320-
# give the threads some time to fail
321-
threads[0].sleep()
322-
323-
# stop all the threads
324-
for t in threads:
325-
t.stop()
326-
327-
for t in threads:
328-
t.join()
329-
330-
# if a thread segfaults, we don't get here
331-
assert True

0 commit comments

Comments
 (0)