Skip to content

Commit 0841606

Browse files
[3.13] gh-119247: Add macros to use PySequence_Fast safely in free-threaded build (GH-119315) (#119419)
Add `Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST` and `Py_END_CRITICAL_SECTION_SEQUENCE_FAST` macros and update `str.join` to use them. Also add a regression test that would crash reliably without this patch. (cherry picked from commit baf347d) Co-authored-by: Josh {*()} Rosenberg <26495692+MojoVampire@users.noreply.github.com>
1 parent cd39da7 commit 0841606

File tree

4 files changed

+106
-3
lines changed

4 files changed

+106
-3
lines changed

Include/internal/pycore_critical_section.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,26 @@ extern "C" {
108108
_PyCriticalSection2_End(&_cs2); \
109109
}
110110

111+
// Specialized version of critical section locking to safely use
112+
// PySequence_Fast APIs without the GIL. For performance, the argument *to*
113+
// PySequence_Fast() is provided to the macro, not the *result* of
114+
// PySequence_Fast(), which would require an extra test to determine if the
115+
// lock must be acquired.
116+
# define Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(original) \
117+
{ \
118+
PyObject *_orig_seq = _PyObject_CAST(original); \
119+
const bool _should_lock_cs = PyList_CheckExact(_orig_seq); \
120+
_PyCriticalSection _cs; \
121+
if (_should_lock_cs) { \
122+
_PyCriticalSection_Begin(&_cs, &_orig_seq->ob_mutex); \
123+
}
124+
125+
# define Py_END_CRITICAL_SECTION_SEQUENCE_FAST() \
126+
if (_should_lock_cs) { \
127+
_PyCriticalSection_End(&_cs); \
128+
} \
129+
}
130+
111131
// Asserts that the mutex is locked. The mutex must be held by the
112132
// top-most critical section otherwise there's the possibility
113133
// that the mutex would be swalled out in some code paths.
@@ -137,6 +157,8 @@ extern "C" {
137157
# define Py_END_CRITICAL_SECTION()
138158
# define Py_BEGIN_CRITICAL_SECTION2(a, b)
139159
# define Py_END_CRITICAL_SECTION2()
160+
# define Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(original)
161+
# define Py_END_CRITICAL_SECTION_SEQUENCE_FAST()
140162
# define _Py_CRITICAL_SECTION_ASSERT_MUTEX_LOCKED(mutex)
141163
# define _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op)
142164
#endif /* !Py_GIL_DISABLED */
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import sys
2+
import unittest
3+
4+
from itertools import cycle
5+
from threading import Event, Thread
6+
from unittest import TestCase
7+
8+
from test.support import threading_helper
9+
10+
@threading_helper.requires_working_threading()
11+
class TestStr(TestCase):
12+
def test_racing_join_extend(self):
13+
'''Test joining a string being extended by another thread'''
14+
l = []
15+
ITERS = 100
16+
READERS = 10
17+
done_event = Event()
18+
def writer_func():
19+
for i in range(ITERS):
20+
l.extend(map(str, range(i)))
21+
l.clear()
22+
done_event.set()
23+
def reader_func():
24+
while not done_event.is_set():
25+
''.join(l)
26+
writer = Thread(target=writer_func)
27+
readers = []
28+
for x in range(READERS):
29+
reader = Thread(target=reader_func)
30+
readers.append(reader)
31+
reader.start()
32+
33+
writer.start()
34+
writer.join()
35+
for reader in readers:
36+
reader.join()
37+
38+
def test_racing_join_replace(self):
39+
'''
40+
Test joining a string of characters being replaced with ephemeral
41+
strings by another thread.
42+
'''
43+
l = [*'abcdefg']
44+
MAX_ORDINAL = 1_000
45+
READERS = 10
46+
done_event = Event()
47+
48+
def writer_func():
49+
for i, c in zip(cycle(range(len(l))),
50+
map(chr, range(128, MAX_ORDINAL))):
51+
l[i] = c
52+
done_event.set()
53+
54+
def reader_func():
55+
while not done_event.is_set():
56+
''.join(l)
57+
''.join(l)
58+
''.join(l)
59+
''.join(l)
60+
61+
writer = Thread(target=writer_func)
62+
readers = []
63+
for x in range(READERS):
64+
reader = Thread(target=reader_func)
65+
readers.append(reader)
66+
reader.start()
67+
68+
writer.start()
69+
writer.join()
70+
for reader in readers:
71+
reader.join()
72+
73+
74+
if __name__ == "__main__":
75+
unittest.main()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added ``Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST`` and
2+
``Py_END_CRITICAL_SECTION_SEQUENCE_FAST`` macros to make it possible to use
3+
PySequence_Fast APIs safely when free-threaded, and update str.join to work
4+
without the GIL using them.

Objects/unicodeobject.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
4444
#include "pycore_bytesobject.h" // _PyBytes_Repeat()
4545
#include "pycore_ceval.h" // _PyEval_GetBuiltin()
4646
#include "pycore_codecs.h" // _PyCodec_Lookup()
47+
#include "pycore_critical_section.h" // Py_*_CRITICAL_SECTION_SEQUENCE_FAST
4748
#include "pycore_format.h" // F_LJUST
4849
#include "pycore_initconfig.h" // _PyStatus_OK()
4950
#include "pycore_interp.h" // PyInterpreterState.fs_codec
@@ -9559,13 +9560,14 @@ PyUnicode_Join(PyObject *separator, PyObject *seq)
95599560
return NULL;
95609561
}
95619562

9562-
/* NOTE: the following code can't call back into Python code,
9563-
* so we are sure that fseq won't be mutated.
9564-
*/
9563+
Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(seq);
95659564

95669565
items = PySequence_Fast_ITEMS(fseq);
95679566
seqlen = PySequence_Fast_GET_SIZE(fseq);
95689567
res = _PyUnicode_JoinArray(separator, items, seqlen);
9568+
9569+
Py_END_CRITICAL_SECTION_SEQUENCE_FAST();
9570+
95699571
Py_DECREF(fseq);
95709572
return res;
95719573
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy