Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Lib/test/test_free_threading/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,35 @@ def sizeof(barrier, b, *ignore):
class CBytesIOTest(ThreadSafetyMixin, TestCase):
ioclass = io.BytesIO

@threading_helper.requires_working_threading()
@threading_helper.reap_threads
def test_concurrent_whole_buffer_read_and_resize(self):
shared = self.ioclass(b"x" * 64)
writers = 2
readers = 8
loops = 2000
barrier = threading.Barrier(writers + readers)

def writer():
barrier.wait()
for i in range(loops):
shared.seek(0)
shared.write(b"a" * (64 + (i & 63)))

def reader():
barrier.wait()
for _ in range(loops):
shared.seek(0)
shared.read()
shared.seek(0)
shared.peek()
shared.getvalue()

threads = [threading.Thread(target=writer) for _ in range(writers)]
threads += [threading.Thread(target=reader) for _ in range(readers)]
with threading_helper.start_threads(threads):
pass

class PyBytesIOTest(ThreadSafetyMixin, TestCase):
ioclass = pyio.BytesIO

Expand Down
5 changes: 4 additions & 1 deletion Lib/test/test_io/test_memoryio.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,10 @@ def test_setstate(self):

@support.cpython_only
def test_sizeof(self):
basesize = support.calcobjsize('P2n2Pn')
if support.Py_GIL_DISABLED:
basesize = support.calcobjsize('P2n2Pni')
else:
basesize = support.calcobjsize('P2n2Pn')
check = self.check_sizeof
self.assertEqual(object.__sizeof__(io.BytesIO()), basesize)
check(io.BytesIO(), basesize )
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix a data race in :class:`io.BytesIO` in free-threaded builds when
whole-buffer reads or peeks, or :meth:`~io.BytesIO.getvalue`, share the
internal buffer with concurrent writes.
49 changes: 47 additions & 2 deletions Modules/_io/bytesio.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ typedef struct {
PyObject *dict;
PyObject *weakreflist;
Py_ssize_t exports;
#ifdef Py_GIL_DISABLED
int buf_shared;
#endif
} bytesio;

#define bytesio_CAST(op) ((bytesio *)(op))
Expand Down Expand Up @@ -71,7 +74,45 @@ check_exports(bytesio *self)
return NULL; \
}

#ifdef Py_GIL_DISABLED
#define SHARED_BUF(self) ((self)->buf_shared || !_PyObject_IsUniquelyReferenced((self)->buf))
#else
#define SHARED_BUF(self) (!_PyObject_IsUniquelyReferenced((self)->buf))
#endif

static inline void
set_shared_buf(bytesio *self)
{
#ifdef Py_GIL_DISABLED
self->buf_shared = 1;
#endif
}

static inline void
clear_shared_buf(bytesio *self)
{
#ifdef Py_GIL_DISABLED
self->buf_shared = 0;
#endif
}

static int
resize_unshared_buffer_lock_held(bytesio *self, Py_ssize_t size)
{
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);

#ifdef Py_GIL_DISABLED
/* If the internal bytes object escaped via a zero-copy getvalue(), read(),
or peek(), resizing it would mutate an object visible to Python code.
Callers must detach first. */
assert(!self->buf_shared);
#endif
int ret = _PyBytes_Resize(&self->buf, size);
if (ret == 0) {
clear_shared_buf(self);
}
return ret;
}


/* Internal routine to get a line from the buffer of a BytesIO
Expand Down Expand Up @@ -128,6 +169,7 @@ unshare_buffer_lock_held(bytesio *self, size_t size)
memcpy(PyBytes_AS_STRING(new_buf), PyBytes_AS_STRING(self->buf),
self->string_size);
Py_SETREF(self->buf, new_buf);
clear_shared_buf(self);
return 0;
}

Expand Down Expand Up @@ -173,7 +215,7 @@ resize_buffer_lock_held(bytesio *self, size_t size)
return -1;
}
else {
if (_PyBytes_Resize(&self->buf, alloc) < 0)
if (resize_unshared_buffer_lock_held(self, alloc) < 0)
return -1;
}

Expand Down Expand Up @@ -381,10 +423,11 @@ _io_BytesIO_getvalue_impl(bytesio *self)
return NULL;
}
else {
if (_PyBytes_Resize(&self->buf, self->string_size) < 0)
if (resize_unshared_buffer_lock_held(self, self->string_size) < 0)
return NULL;
}
}
set_shared_buf(self);
return Py_NewRef(self->buf);
}

Expand Down Expand Up @@ -433,6 +476,7 @@ read_bytes_lock_held(bytesio *self, Py_ssize_t size)
self->pos == 0 && size == PyBytes_GET_SIZE(self->buf) &&
FT_ATOMIC_LOAD_SSIZE_RELAXED(self->exports) == 0) {
self->pos += size;
set_shared_buf(self);
return Py_NewRef(self->buf);
}

Expand Down Expand Up @@ -1046,6 +1090,7 @@ _io_BytesIO___init___impl(bytesio *self, PyObject *initvalue)
if (initvalue && initvalue != Py_None) {
if (PyBytes_CheckExact(initvalue)) {
Py_XSETREF(self->buf, Py_NewRef(initvalue));
clear_shared_buf(self);
self->string_size = PyBytes_GET_SIZE(initvalue);
}
else {
Expand Down
Loading