Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-126366: Make native generators thread safe #126371

Draft
wants to merge 44 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f8b06ae
Lock the iterable for list.__init__()
ZeroIntensity Nov 3, 2024
cd6f573
Add blurb
ZeroIntensity Nov 3, 2024
49fb9e5
Add a test.
ZeroIntensity Nov 3, 2024
ab087ce
Fix the test.
ZeroIntensity Nov 3, 2024
d332da1
Fix the test (again).
ZeroIntensity Nov 3, 2024
61fa342
Fix doctest.
ZeroIntensity Nov 3, 2024
6b6b8e6
Make generators thread safe.
ZeroIntensity Nov 4, 2024
1b72842
Move test to test_generators.
ZeroIntensity Nov 4, 2024
684ccaf
Update the blurb
ZeroIntensity Nov 4, 2024
9edb265
Remove changes to list tests.
ZeroIntensity Nov 4, 2024
3a1fe3a
Fix the test name.
ZeroIntensity Nov 4, 2024
8e84dd4
Add a lock in the _SEND instruction
ZeroIntensity Nov 6, 2024
517b7f8
Add a test in test_free_threading (by Ken)
ZeroIntensity Nov 6, 2024
6276ae5
Fix failing build.
ZeroIntensity Nov 6, 2024
570d80e
Revert "Lock the iterable for list.__init__()"
ZeroIntensity Nov 30, 2024
29c90eb
Merge branch 'main' into list-init-thread-safety
ZeroIntensity Dec 15, 2024
690f45b
Some small changes to the test.
ZeroIntensity Dec 20, 2024
200ba8b
Update Lib/test/test_free_threading/test_generators.py
ZeroIntensity Dec 20, 2024
51f2778
Lower iteration count.
ZeroIntensity Dec 20, 2024
8dcba72
Merge branch 'list-init-thread-safety' of https://github.com/ZeroInte…
ZeroIntensity Dec 20, 2024
ebdc293
Merge branch 'main' of https://github.com/python/cpython into list-in…
ZeroIntensity Dec 20, 2024
6447a82
Lock _PyGen_yf
ZeroIntensity Dec 24, 2024
efe9f87
Remove name change.
ZeroIntensity Dec 24, 2024
55ea54d
Use a critical section for close()
ZeroIntensity Dec 24, 2024
17b7acf
Fix races with gi_frame_state
ZeroIntensity Dec 24, 2024
ccc65c6
Rerun clinic.
ZeroIntensity Dec 24, 2024
192b6a3
Put a critical section around bytecodes that access gi_frame_state
ZeroIntensity Dec 24, 2024
2da4678
Add more to the test.
ZeroIntensity Dec 24, 2024
b34cde6
Add a comment.
ZeroIntensity Dec 24, 2024
2d03a13
Prevent logs from getting spammed.
ZeroIntensity Dec 24, 2024
80c3547
Update Objects/genobject.c
ZeroIntensity Dec 25, 2024
ee93193
Rerun clinic.
ZeroIntensity Dec 25, 2024
d6cc59a
Rename _gen_throw to gen_throw_lock_held
ZeroIntensity Dec 25, 2024
afbb669
Add helper for gen_close
ZeroIntensity Dec 25, 2024
6e45597
Rename to gen_getframe_lock_held
ZeroIntensity Dec 25, 2024
99105e7
Use argument clinic for some more things.
ZeroIntensity Dec 25, 2024
560df73
Fix some coroutine thread safety.
ZeroIntensity Dec 25, 2024
7cd787f
Fix inconsistency with naming.
ZeroIntensity Dec 25, 2024
77a33bb
Add tests for coroutines.
ZeroIntensity Dec 25, 2024
173ba3b
Add some locking assertions.
ZeroIntensity Dec 28, 2024
770ce09
Don't try and re-lock the generator to clear it.
ZeroIntensity Dec 28, 2024
81e90bf
Add tests for async generators.
ZeroIntensity Dec 28, 2024
415f081
Revert "Don't try and re-lock the generator to clear it."
ZeroIntensity Dec 30, 2024
5689653
Merge branch 'main' of https://github.com/python/cpython into list-in…
ZeroIntensity Dec 30, 2024
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
23 changes: 23 additions & 0 deletions Lib/test/test_free_threading/test_generators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import unittest
import concurrent.futures

from unittest import TestCase

from test.support import threading_helper

@threading_helper.requires_working_threading()
class TestGen(TestCase):
def test_generators_basic(self):
def gen():
for _ in range(5000):
ZeroIntensity marked this conversation as resolved.
Show resolved Hide resolved
yield


it = gen()
with concurrent.futures.ThreadPoolExecutor() as executor:
for _ in range(5000):
ZeroIntensity marked this conversation as resolved.
Show resolved Hide resolved
executor.submit(lambda: next(it))


if __name__ == "__main__":
unittest.main()
42 changes: 42 additions & 0 deletions Lib/test/test_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import types

from test import support
from test.support import threading_helper

try:
import _testcapi
Expand Down Expand Up @@ -268,6 +269,47 @@ def loop():
#This should not raise
loop()

@support.requires_resource("cpu")
def test_generator_thread_safety(self):
# GH-126369: generators were not thread safe
from threading import Thread, Lock
import contextlib

def my_generator():
for i in range(1000000):
ZeroIntensity marked this conversation as resolved.
Show resolved Hide resolved
yield i


lock = Lock()
amount = 0
thread_count = 10

def gen_to_list(gen):
# Note: it's intentional to throw out the exception here. Generators
# aren't thread safe, so who knows what will happen. Right now, it just spams
# a lot of ValueError's, but that might change if we decide to make generators
# thread safe in the future. We're just making sure it doesn't crash.
with contextlib.suppress(ValueError):
list(gen)

with lock:
nonlocal amount
amount += 1

generator = my_generator()
with threading_helper.catch_threading_exception() as cm:
ts = [Thread(target=gen_to_list, args=(generator,)) for _ in range(thread_count)]
for t in ts:
t.start()

for t in ts:
t.join()

self.assertIsNone(cm.exc_value)

self.assertEqual(amount, thread_count)



class ModifyUnderlyingIterableTest(unittest.TestCase):
iterables = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix possible data races when using :term:`generator iterator` objects concurrently.
13 changes: 12 additions & 1 deletion Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ gen_dealloc(PyObject *self)
}

static PySendResult
gen_send_ex2(PyGenObject *gen, PyObject *arg, PyObject **presult,
gen_send_ex2_lock_held(PyGenObject *gen, PyObject *arg, PyObject **presult,
int exc, int closing)
{
PyThreadState *tstate = _PyThreadState_GET();
Expand Down Expand Up @@ -273,6 +273,17 @@ gen_send_ex2(PyGenObject *gen, PyObject *arg, PyObject **presult,
return result ? PYGEN_RETURN : PYGEN_ERROR;
}

static inline PySendResult
gen_send_ex2(PyGenObject *gen, PyObject *arg, PyObject **presult,
int exc, int closing)
{
PySendResult result;
Py_BEGIN_CRITICAL_SECTION(gen);
ZeroIntensity marked this conversation as resolved.
Show resolved Hide resolved
result = gen_send_ex2_lock_held(gen, arg, presult, exc, closing);
Py_END_CRITICAL_SECTION();
return result;
}

static PySendResult
PyGen_am_send(PyObject *self, PyObject *arg, PyObject **result)
{
Expand Down
4 changes: 2 additions & 2 deletions Objects/listobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1362,9 +1362,9 @@ _list_extend(PyListObject *self, PyObject *iterable)
Py_END_CRITICAL_SECTION2();
}
else {
Py_BEGIN_CRITICAL_SECTION(self);
Py_BEGIN_CRITICAL_SECTION2(self, iterable);
ZeroIntensity marked this conversation as resolved.
Show resolved Hide resolved
res = list_extend_iter_lock_held(self, iterable);
Py_END_CRITICAL_SECTION();
Py_END_CRITICAL_SECTION2();
}
return res;
}
Expand Down
5 changes: 4 additions & 1 deletion Python/bytecodes.c
Original file line number Diff line number Diff line change
Expand Up @@ -1104,8 +1104,10 @@ dummy_func(
(Py_TYPE(receiver_o) == &PyGen_Type || Py_TYPE(receiver_o) == &PyCoro_Type) &&
((PyGenObject *)receiver_o)->gi_frame_state < FRAME_EXECUTING)
{
_PyInterpreterFrame *gen_frame;
ZeroIntensity marked this conversation as resolved.
Show resolved Hide resolved
PyGenObject *gen = (PyGenObject *)receiver_o;
_PyInterpreterFrame *gen_frame = &gen->gi_iframe;
Py_BEGIN_CRITICAL_SECTION(gen);
ZeroIntensity marked this conversation as resolved.
Show resolved Hide resolved
gen_frame = &gen->gi_iframe;
STACK_SHRINK(1);
_PyFrame_StackPush(gen_frame, v);
gen->gi_frame_state = FRAME_EXECUTING;
Expand All @@ -1115,6 +1117,7 @@ dummy_func(
frame->return_offset = (uint16_t)(INSTRUCTION_SIZE + oparg);
assert(gen_frame->previous == NULL);
gen_frame->previous = frame;
Py_END_CRITICAL_SECTION();
DISPATCH_INLINED(gen_frame);
}
if (PyStackRef_Is(v, PyStackRef_None) && PyIter_Check(receiver_o)) {
Expand Down
9 changes: 8 additions & 1 deletion Python/generated_cases.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading