GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — gh-123271: Make builtin zip method safe under free-threading by eendebakpt · Pull Request #123272 · python/cpython Conversation
| ||||||||||||||||||||||||||||||||||||||||||||||
| @@ -0,0 +1 @@ | |||
| Make :meth:`zip` safe under free-threading. | |||
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
| Make :meth:`zip` safe under free-threading. | |
| Make :func:`zip` thread-safe without the :term:`GIL`. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Even with this PR the zip function is not thread-safe: when iterating over zip(range(100), range(100)) one can obtain a tuple (2,3). What does PR (hopefully) does is modifying the code so that the interpreter does not crash. For this reason I would like to be careful with the term "thread-safe". I choose not to make zip fully thread safe because of the performance impact, see the discussion in #120496
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Thanks for clarifying. The word safe is a bit confusing, I guess a lot of other people would read it as thread-safe. I think we need to clarify the reason why zip isn't safe in free-threaded build.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Co-authored-by: Kirill Podoprigora <[email protected]>
| #ifdef Py_GIL_DISABLED | ||
| int reuse_tuple = 0; | ||
| #else | ||
| int reuse_tuple = Py_REFCNT(result) == 1; | ||
| #endif | ||
| if (reuse_tuple) { |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
I think we want to enable re-use under certain conditions. We should probably put the logic in some internal function, possibly in pycore_object.h. I think the condition for the free-threaded build is:
_Py_ThreadId()The logic is that no other thread calling zip_next can re-use lz->result because of the thread id condition (condition 1). And no thread outside of zip_next can incref that object because the combined conditions ensure that lz->result is the only reference.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
@colesbury Thanks for the hint! I implementeded the suggestion
_Py_Reuse_Immutable_Object so we can use the trick code for several other cases (e.g. itertools.pairwise, enumerate). The location and name of the new method can be changed thoughob_ref_local is most informative and fastest to checkob_ref_shared to be zero, but perhaps we should check on Py_ARITHMETIC_RIGHT_SHIFT(Py_ssize_t, ob_ref_shared , _Py_REF_SHARED_SHIFT)?
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
…e-123271.xeVViR.rst Co-authored-by: Sam Gross <[email protected]>
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
Thanks @eendebakpt. The check that ob_ref_shared is entirely zero is correct as you wrote it. We don't want non-zero flags in this case as that could allow another thread to concurrently incref the object in some circumstances.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
| _ = [t.start() for t in worker_threads] | ||
| _ = [t.join() for t in worker_threads] |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
The preferred style is to use for loops instead of using list comprehensions for side effects.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
| number_of_threads = 8 | ||
| number_of_iterations = 40 | ||
| n = 40_000 |
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
We run lots of unit tests in CI, so they individually need to be very fast. For the free-threaded tests, we should aim for <0.1 seconds on two cores (i.e., when run with taskset -c 0-1 on Linux)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
On my system (windows) the test is 0.07 seconds for the free-threading build. I will reduce the number of threads and iterations a bit so it is even faster.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
Co-authored-by: Sam Gross <[email protected]>
|
Thanks for the fix @eendebakpt! There's maybe a dozen or so similar patterns in CPython and I searched for the regex |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
@colesbury Thanks for the guidance here! I was indeed planning on addressing some of the similar patterns. I'll open an new issue for this. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.