Skip to content

Add a public, fail-able Py_INCREF for the free-threaded build #113920

Closed
@dpdani

Description

@dpdani

Feature or enhancement

Proposal:

C extensions targeting the free-threaded build will incur the issue of trying to incref a PyObject that's being concurrently decrefed, and possibly deallocated, by another thread.

This is a normal situation in concurrent reference counting, but ATM in main there's no public C API to handle it.
To handle references to objects that may be concurrently de-allocated, there needs to be a routine that attempts to increment the reference count and returns a failure status when such incref is not safe.
The caller is then supposed to handle the failure according to its own logic.

There is a function called _Py_TRY_INCREF in the original nogil fork that does precisely this and is mentioned in PEP-703, but is marked as private.
The implementation in the nogil-3.12 fork differs, and there additionally is another function, with different semantics.

I'll briefly provide a somewhat concrete example taken from my library cereggii here (makes use of nogil-3.9). Consider the case where a reference to a PyObject is stored inside another PyObject,1 and that can be modified by more than one thread:

PyObject *AtomicRef_Get(AtomicRef *self)
{
    PyObject *reference;
    reference = self->reference;  // L1
    while (!_Py_TRY_INCREF(reference)) {  // L2
        reference = self->reference;
    }
    return reference;
}

Between lines L1 and L2 it could be that another thread decrements the refcount of the referenced object and deallocates it.

If Py_INCREF was used (unconditionally) instead of _Py_TRY_INCREF, the interpreter could encounter a segmentation fault if the memory page was freed, or, possibly even worse, the location at which the referenced object was stored could be recycled for another PyObject and suddenly this example method returns an object that was never stored inside self->reference.

This issue is of course due to the fact that no lock protects the reference count of objects and their deallocation, and is a normal issue of concurrent reference counting.

I know there's a lot of work still to be done on the free-threaded build, but I think there can be a general-enough interest among C extension authors to have such a fail-able Py_INCREF public ABI at disposal to justify adding this item to the to-do list.

During a brief exchange with @colesbury, he noted to me that the implementation present in nogil-3.12 is quite difficult to explain, and thus a bad candidate for a public ABI.
Though, the public macro doesn't necessarily have to be what CPython uses internally, it could just as well be something purposefully made for C extensions.

I'm willing to invest some time in porting the conditional incref functions of nogil-3.12 into main, and possibly add another public C API, if it can be helpful. (This isn't specifically tracked in #108219, I think Sam wants to implement it before or while working on dict/list.)

Also, notice that compatibility with the default build can achieved trivially:

#if !defined(Py_NOGIL)
#  define Py_TRY_INCREF Py_INCREF
#endif

Has this already been discussed elsewhere?

No response given

Links to previous discussion of this feature:

https://discuss.python.org/t/towards-a-python-util-concurrent/41325/16

Footnotes

  1. As it is in this case, but the same reasoning applies to any other container data structure as well.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions