Skip to content

Commit b5e17e1

Browse files
committed
gh-153928: make Unicode iterators thread-safe
1 parent 948fd7e commit b5e17e1

2 files changed

Lines changed: 114 additions & 17 deletions

File tree

Lib/test/test_free_threading/test_iteration.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,46 @@ def assert_iterator_results(self, results, expected):
125125
extra_items = set(results) - set(expected)
126126
for item in extra_items:
127127
self.assertEqual((item - expected.start) % expected.step, 0)
128+
129+
class ContendedStringIterationTest(ContendedTupleIterationTest):
130+
def make_testdata(self, n):
131+
return "A" * n
132+
133+
def test_shared_iterator(self):
134+
"""Test iteration over a shared string iterator"""
135+
seq = self.make_testdata(NUMITEMS)
136+
it = iter(seq)
137+
results = []
138+
start = threading.Barrier(NUMTHREADS)
139+
140+
def worker():
141+
items = []
142+
start.wait()
143+
for item in it:
144+
items.append(item)
145+
results.extend(items)
146+
147+
threads = self.run_threads(worker)
148+
for t in threads:
149+
t.join()
150+
151+
self.assert_iterator_results(results, seq)
152+
153+
def test_shared_iterator_exhaustion(self):
154+
"""Test concurrent exhaustion of a shared string iterator"""
155+
for _ in range(100):
156+
seq = self.make_testdata(NUMITEMS)
157+
it = iter(seq)
158+
start = threading.Barrier(NUMTHREADS)
159+
160+
def worker():
161+
start.wait()
162+
while True:
163+
try:
164+
next(it)
165+
except StopIteration:
166+
return
167+
168+
threads = self.run_threads(worker)
169+
for t in threads:
170+
t.join()

Objects/unicodeobject.c

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
5555
#include "pycore_pyhash.h" // _Py_HashSecret_t
5656
#include "pycore_pylifecycle.h" // _Py_SetFileSystemEncoding()
5757
#include "pycore_pystate.h" // _PyInterpreterState_GET()
58+
#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_*()
59+
#include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION()
5860
#include "pycore_ucnhash.h" // _PyUnicode_Name_CAPI
5961
#include "pycore_unicodectype.h" // _PyUnicode_IsXidStart
6062
#include "pycore_unicodeobject.h" // struct _Py_unicode_state
@@ -14968,21 +14970,30 @@ unicodeiter_next(PyObject *op)
1496814970
PyObject *seq;
1496914971

1497014972
assert(it != NULL);
14973+
#ifdef Py_GIL_DISABLED
14974+
seq = _Py_atomic_load_ptr(&it->it_seq);
14975+
#else
1497114976
seq = it->it_seq;
14977+
#endif
1497214978
if (seq == NULL)
1497314979
return NULL;
1497414980
assert(_PyUnicode_CHECK(seq));
1497514981

14976-
if (it->it_index < PyUnicode_GET_LENGTH(seq)) {
14982+
Py_ssize_t index = FT_ATOMIC_LOAD_SSIZE_RELAXED(it->it_index);
14983+
if (index < PyUnicode_GET_LENGTH(seq)) {
1497714984
int kind = PyUnicode_KIND(seq);
1497814985
const void *data = PyUnicode_DATA(seq);
14979-
Py_UCS4 chr = PyUnicode_READ(kind, data, it->it_index);
14980-
it->it_index++;
14986+
Py_UCS4 chr = PyUnicode_READ(kind, data, index);
14987+
FT_ATOMIC_STORE_SSIZE_RELAXED(it->it_index, index + 1);
1498114988
return unicode_char(chr);
1498214989
}
1498314990

14991+
#ifdef Py_GIL_DISABLED
14992+
seq = _Py_atomic_exchange_ptr(&it->it_seq, NULL);
14993+
#else
1498414994
it->it_seq = NULL;
14985-
Py_DECREF(seq);
14995+
#endif
14996+
Py_XDECREF(seq);
1498614997
return NULL;
1498714998
}
1498814999

@@ -14991,21 +15002,32 @@ unicode_ascii_iter_next(PyObject *op)
1499115002
{
1499215003
unicodeiterobject *it = (unicodeiterobject *)op;
1499315004
assert(it != NULL);
15005+
#ifdef Py_GIL_DISABLED
15006+
PyObject *seq = _Py_atomic_load_ptr(&it->it_seq);
15007+
#else
1499415008
PyObject *seq = it->it_seq;
15009+
#endif
1499515010
if (seq == NULL) {
1499615011
return NULL;
1499715012
}
1499815013
assert(_PyUnicode_CHECK(seq));
1499915014
assert(PyUnicode_IS_COMPACT_ASCII(seq));
15000-
if (it->it_index < PyUnicode_GET_LENGTH(seq)) {
15015+
15016+
Py_ssize_t index = FT_ATOMIC_LOAD_SSIZE_RELAXED(it->it_index);
15017+
if (index < PyUnicode_GET_LENGTH(seq)) {
1500115018
const void *data = ((void*)(_PyASCIIObject_CAST(seq) + 1));
15002-
Py_UCS1 chr = (Py_UCS1)PyUnicode_READ(PyUnicode_1BYTE_KIND,
15003-
data, it->it_index);
15004-
it->it_index++;
15019+
Py_UCS1 chr = (Py_UCS1)PyUnicode_READ(
15020+
PyUnicode_1BYTE_KIND, data, index);
15021+
FT_ATOMIC_STORE_SSIZE_RELAXED(it->it_index, index + 1);
1500515022
return (PyObject*)&_Py_SINGLETON(strings).ascii[chr];
1500615023
}
15024+
15025+
#ifdef Py_GIL_DISABLED
15026+
seq = _Py_atomic_exchange_ptr(&it->it_seq, NULL);
15027+
#else
1500715028
it->it_seq = NULL;
15008-
Py_DECREF(seq);
15029+
#endif
15030+
Py_XDECREF(seq);
1500915031
return NULL;
1501015032
}
1501115033

@@ -15014,8 +15036,22 @@ unicodeiter_len(PyObject *op, PyObject *Py_UNUSED(ignored))
1501415036
{
1501515037
unicodeiterobject *it = (unicodeiterobject *)op;
1501615038
Py_ssize_t len = 0;
15039+
#ifdef Py_GIL_DISABLED
15040+
PyObject *seq;
15041+
Py_BEGIN_CRITICAL_SECTION(it);
15042+
seq = Py_XNewRef(it->it_seq);
15043+
Py_END_CRITICAL_SECTION();
15044+
if (seq != NULL) {
15045+
Py_ssize_t index = FT_ATOMIC_LOAD_SSIZE_RELAXED(it->it_index);
15046+
Py_ssize_t seq_len = PyUnicode_GET_LENGTH(seq);
15047+
if (index < seq_len)
15048+
len = seq_len - index;
15049+
Py_DECREF(seq);
15050+
}
15051+
#else
1501715052
if (it->it_seq)
1501815053
len = PyUnicode_GET_LENGTH(it->it_seq) - it->it_index;
15054+
#endif
1501915055
return PyLong_FromSsize_t(len);
1502015056
}
1502115057

@@ -15031,16 +15067,34 @@ unicodeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
1503115067
* call must be before access of iterator pointers.
1503215068
* see issue #101765 */
1503315069

15070+
#ifdef Py_GIL_DISABLED
15071+
PyObject *seq;
15072+
Py_ssize_t index;
15073+
Py_BEGIN_CRITICAL_SECTION(it);
15074+
seq = Py_XNewRef(it->it_seq);
15075+
index = FT_ATOMIC_LOAD_SSIZE_RELAXED(it->it_index);
15076+
Py_END_CRITICAL_SECTION();
15077+
15078+
if (seq != NULL) {
15079+
if (index < PyUnicode_GET_LENGTH(seq)) {
15080+
PyObject *result = Py_BuildValue("N(O)n", iter, seq, index);
15081+
Py_DECREF(seq);
15082+
return result;
15083+
}
15084+
Py_DECREF(seq);
15085+
}
15086+
#else
1503415087
if (it->it_seq != NULL) {
1503515088
return Py_BuildValue("N(O)n", iter, it->it_seq, it->it_index);
15036-
} else {
15037-
PyObject *u = _PyUnicode_GetEmpty();
15038-
if (u == NULL) {
15039-
Py_XDECREF(iter);
15040-
return NULL;
15041-
}
15042-
return Py_BuildValue("N(N)", iter, u);
1504315089
}
15090+
#endif
15091+
15092+
PyObject *u = _PyUnicode_GetEmpty();
15093+
if (u == NULL) {
15094+
Py_XDECREF(iter);
15095+
return NULL;
15096+
}
15097+
return Py_BuildValue("N(N)", iter, u);
1504415098
}
1504515099

1504615100
PyDoc_STRVAR(reduce_doc, "Return state information for pickling.");
@@ -15057,7 +15111,7 @@ unicodeiter_setstate(PyObject *op, PyObject *state)
1505715111
index = 0;
1505815112
else if (index > PyUnicode_GET_LENGTH(it->it_seq))
1505915113
index = PyUnicode_GET_LENGTH(it->it_seq); /* iterator truncated */
15060-
it->it_index = index;
15114+
FT_ATOMIC_STORE_SSIZE_RELAXED(it->it_index, index);
1506115115
}
1506215116
Py_RETURN_NONE;
1506315117
}

0 commit comments

Comments
 (0)