-
Notifications
You must be signed in to change notification settings - Fork 1
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
Make critical section API public as part of non-limited C API #26
Comments
Macros are only usable in C/C++. To lift that limitation, the underlying type & functions would need to be made public as well, and since we expect that non-C language wrappers re-implement the macros, the documentation should show what they expand to -- similar to That complicates things, of course -- the current implementation requires the size of the |
The functions are callable from Rust and other languages. The structs are two pointers ( Note that C API types are not really usable outside of C/C++ either. PyO3, for example, reimplements all the C structs in Rust. The complaint with |
I'm fine with these 4 macros. It's ok that macros cannot be used in Rust and other programming languages, if |
These macros look similar to |
So, we want to expose:
(The actual expansion might be different, e.g. no-op in the default build, but these should be tested.) Is that right?
Yup, that's why we want to avoid them where possible. |
IMO members should be public. Trying to provide a stable ABI is too early, and trying to hide members would only make the implementation more complicated and fragile. The issue title clearly excludes the limited C API:
|
Right, I meant private rather than opaque (edited). The compiler will see them, users should not touch them directly. |
A few differences between what @encukou wrote and the current PR:
|
Yup, I do want to make them public, so that non-C languages can use them. I'd also like the functions to be exposed as actual DLL symbols (and shadowed with I edited the post to add the casts. |
The functions are exposed as actual DLL symbols in the linked PR. The inline functions aren't public (i.e., they are in There's a dance with redefining the |
I suggest you to use a different name for the "fast inline" flavor. I did something similar with PyThreadState_GET():
|
The #26 (comment) API LGTM. |
It's fine for me for non-limited C API as well. But unlike PyMutex, I think we want to add this one to the limited API rather soon, and we should plan for that. So let me look at the aspects that tend to prevent evolution. Exposed struct sizesThe usual solution for avoiding struct sizes is adding API to allocate the struct. But since there's already space for the pointer, we can (if it's ever needed) make Or perhaps we can do what was proposed for PyMutex: inflate the struct size to give some room for expansion. Error reportingIt seems that failure to enter/exit a critical section is not really recoverable, but generally it's good if functions are able to raise runtime deprecation warnings. |
Allocating memory on the heap means introducing the risk of a memory allocation failure. I don't think that we want that for a critical section. |
We are trying to reduce the stack memory usage in Python. I don't think that adding an unused member "just in case" (for future usage) to the structure is worth it. |
To me, the possibility of PyMutex needing to expand in size seems too uncertain to worry about it. The basic lock functionality will not require extra space, ever. (If they ever do, they will have to invent a new type.) The only reason to reserve extra space seems to be the idea of possibly having the lock contain a pointer to the "real" location of the lock, in case an object containing a lock is moved by a hypothetical future GC. But this feels like the wrong solution for that problem: It would make more sense for the GC to declare locked objects unmovable (there will always be reasons why a particular object can't be moved, so this would just be another reason). Also, the Stable ABI would have many other problems when GC starts moving objects, so it's also a straw man. |
It would be helpful to get a decision on this API. It seems to me that people are supportive of the API @encukou described in #26 (comment). There is still some uncertainty about future stable ABI considerations, but the current proposal does not affect the stable ABI/limited C API. |
Yeah, let's do a formal vote for the API in #26 (comment) |
This looks like an absolute nightmare to debug (it's the first time I've seen how these work), and unfortunately it's not going to solve the existing race conditions/vulnerabilities we have that I'd hoped per-object locks would help with, but the API (the macro names) are fine. I don't see how this gets into the stable ABI at all in the future, though, nor can this be used reliably from other languages (for an arbitrary value of "other language"). If we want to go there, I think we'll need a design that doesn't rely on pointers to user-defined variables on the stack, and probably need to store it all in our thread state and return a key to the caller to store (this will be hidden in the macro for C devs, so their sources don't change). |
@colesbury: Congrats, your API got approved, I close the issue. @zooba: Maybe some tooling can be added later to help debugging dead locks and race conditions. And yeah, for now, this API doesn't belong to the stable ABI, and I agree that a redesign may be needed if we want to make it happen later. |
The critical section API provides macros for locking one or two Python objects at a time. I propose making the following macros public as part of
Include/cpython
(i.e., the non-limited C API):Py_BEGIN_CRITICAL_SECTION(PyObject *op)
/Py_END_CRITICAL_SECTION()
Py_BEGIN_CRITICAL_SECTION2(PyObject *a, PyObject *b)
/Py_END_CRITICAL_SECTION2()
These macros are defined, but no-ops, in the default build.
PR draft: python/cpython#119353
Public header: Include/cpython/critical_section.h
PEP 703 section: https://peps.python.org/pep-0703/#python-critical-sections
Note that the PR draft doesn't currently have user facing documentation. I'll add that, but I'm hoping to get a decision on the proposed API first.
Why is this useful?
C API extensions may need to add synchronization to be thread-safe without the GIL. In some cases, straightforward per-object locking can introduce deadlocks that were not present when running with the GIL. This API avoids that by implicitly suspending critical sections in places where the GIL would be released.
This makes it easier to add locking to extensions without introducing deadlocks.In the "nogil" fork, we used this to make https://github.com/gaogaotiantian/viztracer thread-safe, and we've also used this extensively within CPython.
Note that in some cases plain mutexes are still more appropriate: the
Py_BEGIN/END_CRITICAL_SECTION
API makes it easier to avoid deadlocks, but plain mutexes make it easier to reason about the invariant that the mutex ensures.Why is this part of CPython?
The underlying implementation hooks in to the PyThreadState management, so it would not be possible to implement this outside of CPython.
The text was updated successfully, but these errors were encountered: