| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
We can avoid stop the world pauses in both cases by checking if the type is uniquely ref by the current thread, the type object is newly created here and not exposed to other threads so we can assign without stopping the world. |
Sorry, something went wrong.
This would be better to be part of the pystats info.
There was a problem hiding this comment.
I'm concerned about atomicity here (see comment below).
Sorry, something went wrong.
| PyInterpreterState *interp = _PyInterpreterState_GET(); | ||
| _PyEval_StopTheWorld(interp); |
There was a problem hiding this comment.
_PyEval_StopTheWorld() is always defined and is a no-op in the default build.
Sorry, something went wrong.
|
We might be able to make BEGIN_TYPE_LOCK() a regular mutex acquisition instead of a critical section, but we'll have to be careful that nothing between BEGIN_TYPE_LOCK() and END_TYPE_LOCK() leads to reentrancy. |
Sorry, something went wrong.
For TYPE_LOCK, replace critical sections with a simple mutex. A number of changes were needed to avoid deadlocking on reentrancy (trying to re-acquire the mutex). Split _PyType_LookupRefAndVersion() into a locked and unlocked version. Remove atomic operations where they are not required. Remove some cases of TYPE_LOCK being held that is not required.
|
I've converted TYPE_LOCK from being used as a critical section to being a regular mutex. There was a number of cases of reentrancy and so things got complex. I think I nearly have that sorted out now. The mro_invoke() function needs work yet. At least, unit tests pass without deadlocking. One somewhat ugly part yet is that _PyType_LookupRefAndVersion() got split into two versions with only slight differences between the functions (taking lock if needed vs caller already holding lock or the lock not being needed). Perhaps I can find a way to refactor it to avoid code duplication. |
Sorry, something went wrong.
There was a problem hiding this comment.
Nice!
I think there's still too much mixing of the type lock and the stop the world. I think data should either be protected by the type lock OR by stop-the-world, but not both.
From reading through this:
type lock:
stop the world:
Additionally, there's a few functions that are named with _lock_held but are called in a stop-the-world pause (not with the type lock held), which I found a bit confusing.
Sorry, something went wrong.
| types_mutex_unlock(); | ||
| new_mro = mro_invoke(type); /* might cause reentrance */ | ||
| types_mutex_lock(); |
There was a problem hiding this comment.
This doesn't seem right to me. This can be called with both the type lock held and the world stopped. If mro_invoke can call arbitrary code, it needs to start the world around the call.
Sorry, something went wrong.
There was a problem hiding this comment.
Yes, that can call a custom mro() method on the meta class. The only thing in Lib that does that is a test_descr test but obviously we need to handle it.
Sorry, something went wrong.
There was a problem hiding this comment.
I believe this has been resolved. Inside mro_invoke() I either start-the-world before calling mro() (if it's custom) or I unlock the mutex. The latter case happens during type_ready().
Sorry, something went wrong.
Okay, I've reverted back to using a critical section for TYPE_LOCK. |
Sorry, something went wrong.
This avoids the watcher callback potentially using invalid cache entries.
* Since type_modified_unlocked() can be called with the world stopped then we need to re-start if making re-entrant calls (like calling type watcher callbacks). * Re-order code before type_modified_unlocked(). This fixes a potential bug if called functions end up re-assigning the type version tag. If they do, the cache can contain invalid entries. We should do the dictionary update first then set the version to zero. * Replace 'is_new_type' with 'world_stopped' boolean parameter. This seems a bit more clear.
Rather than trying to update tp_base, tp_bases, and tp_mro while the world is stopped, hold TYPE_LOCK when updating them. This is much more similar to how the base version of the code works and requires many fewer changes. Using stop-the-world turned out to be difficult to make work correctly, mostly due to custom mro() methods but also for some other reasons (for example, functions that potentially re-enter the interpreter). This re-work requires that we can call stop-the-world inside a critical section and have the mutex still held after re-starting. This seems to work and I've added asserts to confirm it. Stop-the-world is now only used when updating either tp_flags or other type slots and not the three type members listed above.
|
Major rework, reverting back to using TYPE_LOCK and critical sections to protect tp_mro, tp_bases, tp_base. In a previous version of this PR, I was making updates to those only while the world was stopped. Because we are so limited on what APIs you can call while the world is stopped it is hard to make it work without major overhaul to how types work. The mro() method on the metatype is particularly troublesome but there are other similar problems. Here is a summary of what's changed vs the main branch:
|
Sorry, something went wrong.
These are not required as part of this PR and can be reviewed separately.
There was a problem hiding this comment.
This LGTM once the issue with test_opcache is resolved
Sorry, something went wrong.
Now that these slot updates use stop-the-world, these two tests are quite a lot slower. Reduce size of the items list from 1000 to 100.
…thongh-131174) This is triggering deadlocks in test_opcache.
| Back | FazBrowse Home | New Git URL |
Avoid data races when updating type slots and type flags by "stopping-the-world" first. This is only needed if the type is potentially exposed to multiple threads. There are a couple places this happens:
Two examples of code that causes this: dataclasses will assign dunder methods after the type object has been created, when the decorator runs. That triggers this and since the slots are assigned one-by-one, we stop for each assignment. Not great. Another example is unittest MagicMock. Again, it assigns dunder methods after the class is defined.
In order to assess the performance impact of this change, I've measured the following things. The time it takes to run the unit test suite does not seem significantly worse, even when running tests serially using -j 1. Comparing the pyperformance results before and after this change shows no significant change. See link below for the chart.
Running with TSAN enabled and running the --tsan and --tsan-parallel --parallel-threads=4 tests show no warnings. I also have a modified pyperformance test suite that I can run in parallel and that shows no TSAN warnings related to this as well.
Additional changes after feedback from Sam:
Benchmark using pyperformance