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-115999: Make list, tuple and range iteration more thread-safe. #128637

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

Yhg1s
Copy link
Member

@Yhg1s Yhg1s commented Jan 8, 2025

Make list, tuple and range iteration more thread-safe, and actually test concurrent iteration. (This is prep work for enabling specialization of FOR_ITER in free-threaded builds.) The basic premise is:

  • Iterating over a shared iterable (list, tuple or range) should be safe, not involve data races, and behave like iteration normally does.

  • Using a shared iterator should not crash or involve data races, and should only produce items regular iteration would produce. It is not guaranteed to produce all items, or produce each item only once.

Providing stronger guarantees is possible for some of these iterators, but it's not always straight-forward and can significantly hamper the common case. Since iterators in general aren't shared between threads, and it's simply impossible to concurrently use many iterators (like generators), better to make sharing iterators without explicit synchronization clearly wrong.

Specific issues fixed in order to make the tests pass:

  • List iteration could occasionally crash when a shared list wasn't already marked as shared when reallocated.

  • Tuple iteration could occasionally crash when the iterator's reference to the tuple was cleared on exhaustion. Like with list iteration, in free-threaded builds we can't safely and efficiently clear the iterator's reference to the iterable (doing it safely would mean extra, slow refcount operations), so just keep the iterable reference around.

  • Fast range iterators (for integers that fit in C longs) shared between threads would sometimes produce values past the end of the range, because the iterators use two pieces of state that we can't efficiently update atomically. Rewriting the iterators to have a single piece of state is possible, but probably means more math for each iteration and may not be worth it.

  • Long range iterators (for other numbers) shared between threads would crash catastrophically in a variety of ways. This now uses a critical section. Rewriting this to be more efficient is probably possible, but since it deals with arbitrary Python objects it's difficult to get right.

There seem to be no more existing races in list_get_item_ref, so drop it from the tsan suppression list.

concurrent iteration. (This is prep work for enabling specialization of
FOR_ITER in free-threaded builds.) The basic premise is:

 - Iterating over a shared _iterable_ (list, tuple or range) should be safe,
   not involve data races, and behave like iteration normally does.

 - Using a shared _iterator_ should not crash or involve data races, and
   should only produce items regular iteration would produce. It is _not_
   guaranteed to produce all items, or produce each item only once.

Providing stronger guarantees is possible for some of these iterators, but
it's not always straight-forward and can significantly hamper the common
case. Since iterators in general aren't shared between threads, and it's
simply impossible to concurrently use many iterators (like generators),
better to make sharing iterators without explicit synchronization clearly
wrong.

Specific issues fixed in order to make the tests pass:

 - List iteration could occasionally crash when a shared list wasn't already
   marked as shared when reallocated.

 - Tuple iteration could occasionally crash when the iterator's reference to
   the tuple was cleared on exhaustion. Like with list iteration, in
   free-threaded builds we can't safely and efficiently clear the iterator's
   reference to the iterable (doing it safely would mean extra, slow
   refcount operations), so just keep the iterable reference around.

 - Fast range iterators (for integers that fit in C longs) shared between
   threads would sometimes produce values past the end of the range, because
   the iterators use two bits of state that we can't efficiently update
   atomically. Rewriting the iterators to have a single bit of state is
   possible, but probably means more math for each iteration and may not be
   worth it.

 - Long range iterators (for other numbers) shared between threads would
   crash catastrophically in a variety of ways. This now uses a critical
   section. Rewriting this to be more efficient is probably possible, but
   since it deals with arbitrary Python objects it's difficult to get right.

There seem to be no more exising races in list_get_item_ref, so drop it from
the tsan suppression list.
@Yhg1s Yhg1s added the skip news label Jan 8, 2025
Py_INCREF(r->len);
return r->len;
PyObject *len;
Py_BEGIN_CRITICAL_SECTION(r);
Copy link
Member

@corona10 corona10 Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, using atomic operation is not enough for this operation?
(Maybe with _Py_TryIncref?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants