Skip to content

Add thread-safe cache initialization section to free-threading docs#7582

Merged
da-woods merged 7 commits intocython:masterfrom
ngoldbaum:thread-safe-cache-init
Mar 24, 2026
Merged

Add thread-safe cache initialization section to free-threading docs#7582
da-woods merged 7 commits intocython:masterfrom
ngoldbaum:thread-safe-cache-init

Conversation

@ngoldbaum
Copy link
Copy Markdown
Contributor

Based on my experience working on scipy/scipy#24799.

@ngoldbaum ngoldbaum force-pushed the thread-safe-cache-init branch from 4e18108 to 1e72fe6 Compare March 19, 2026 21:52
def __get__(B self):
def closure():
self._py_obj = A(create_c_object(self.key))
self.cache_flag.store(True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This I think belongs to your later example

self.key = key
self._py_obj = None

property obj:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd probably recommend just using the Python syntax:

@property
def obj(self):
    ...

It should end up as exactly the same code, and we general try to steer people to Python-type syntax where available.

@da-woods
Copy link
Copy Markdown
Contributor

da-woods commented Mar 19, 2026

One more comment (which is maybe more a suggestion to "how to do it in SciPy" than this documentation PR):

you could avoid the closure by using the "args" version of py_safe_call_once. Unfortunately it's a little fiddly to pass self in because we don't allow object to varargs. But I think you could cast it to/from void*. There's no lifetime issues because it's called immediately or not at all. Untested example:

cdef void _initialize_obj(void *self_B):
    (<B>self_B.)_py_obj = A(create_c_object(self.key))


cdef class B:
    ...

    @property
    def obj(self):
        py_safe_call_once(self.flag, _initialize_obj, <void*>self)
        return self._py_obj

I'll have a proper look at the proposed documentation tomorrow

@ngoldbaum
Copy link
Copy Markdown
Contributor Author

The C++ compiler complains:

scipy/spatial/_ckdtree.cpython-314-darwin.so.p/_ckdtree.cpp:11183:5: error: no matching function for call to '__pyx_cpp_py_safe_call_once'
 11183 |     __pyx_cpp_py_safe_call_once<void *>(__pyx_v_self->flag, ((void *)(&__pyx_f_5scipy_7spatial_8_ckdtree_init_pytree)));
       |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
scipy/spatial/_ckdtree.cpython-314-darwin.so.p/_ckdtree.cpp:3780:10: note: candidate function template not viable: expects an lvalue for 2nd argument
 3780 |     void __pyx_cpp_py_safe_call_once(std::once_flag& flag, Callable& callable, Args&&... args) {
      |          ^                                                 ~~~~~~~~~~~~~~~~~~
1 error generated.

return self._py_obj

If you want to avoid the need to define a closure or wrapper object in every
call to ``__get__``, you can also use an atomic boolean flag to indicate whether
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

should be obj now

@da-woods
Copy link
Copy Markdown
Contributor

The C++ compiler complains:

So I think that's because you're casting the function to <void*> rather than the self arg. However... casting the self arg also fails with a different compiler error. I'll do a fix for it and make sure we're testing passing arguments correctly

@da-woods
Copy link
Copy Markdown
Contributor

Fixup to make my proposed example work is in #7585 (together with a small amount of working code that demonstrates it)

@ngoldbaum
Copy link
Copy Markdown
Contributor Author

So I think that's because you're casting the function to <void*> rather than the self arg

Yes, that's wrong. Sorry for the confusion, I was at the end of a long day...

I can confirm that with your Cython branch I'm able to get rid of the closure in SciPy. Are you planning to backport that for 3.2.5? And if so, when were you planning to cut a new bugfix release?

@ngoldbaum
Copy link
Copy Markdown
Contributor Author

It probably makes sense to discuss py_safe_call_once before py_safe_call_object_once. I only used it in these docs because I couldn't figure out how to pass self. The docs should also try to explain when you'd prefer py_safe_call_object_once (e.g. if you need to single-initialize a resource managed by Python code or a Python singleton or something like that.

@da-woods
Copy link
Copy Markdown
Contributor

It probably makes sense to discuss py_safe_call_once before py_safe_call_object_once. I only used it in these docs because I couldn't figure out how to pass self. The docs should also try to explain when you'd prefer py_safe_call_object_once (e.g. if you need to single-initialize a resource managed by Python code or a Python singleton or something like that.

Yeah agree with all of this. I suspect it probably also isn't work having the cache_flag version either (it's really only a workaround for "making a closure is a bit expensive" and is really duplicating some of the logic in once_flag).


I've been trying to think of a way of making passing Python arguments in a bit nicer, but unfortunately I'm not coming up with many good ideas (especially trying to keep it as a library feature rather than adding anything to the language). void* is a bit hacky but I think it's fine if documented and so people don't have to find it independently.

Copy link
Copy Markdown
Contributor

@da-woods da-woods left a comment

Choose a reason for hiding this comment

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

There's a few bits where I think the code won't quite compile correctly. We've typically put our documentation examples as separate files in the docs/examples directory, and then they at least get compiled (although not run) to verify correctness.

I wonder if the example could be cut down further partly for the sake of simplicity and partly to get it to something compilable without too many extra details.

How about omitting class A and just saying

from somewhere import expensive_python_function

cdef void init_py_obj(void *void_instance):
          cdef B instance = <B>void_instance
          instance._py_obj = expensive_python_function(instance.key)

and then the details of what you're initializing _py_obj with disappear completely.

Comment on lines +183 to +184
def __init__(A self, c_object *obj):
self.obj = c_object
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this'll work because you can't pass C pointers to def functions


cdef class B:
cdef:
int key
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we're missing the declaration for _py_obj here

Comment on lines +239 to +240
example casts to ``void *`` to work around that. This is safe so long as the
``cdef`` initialization function doesn't need any data managed by Python objects.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not quite sure about the "This is safe..." section.

From my point of view it's safe because we hold an actual reference to self for the entire duration of the call_once call

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I adopted a slight tweak of this language, thanks!

Comment on lines +246 to +247
from libcpp.mutex cimport py_safe_call_object_once, py_safe_once_flag

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Indentation looks wrong here

@ngoldbaum
Copy link
Copy Markdown
Contributor Author

Thanks for the pointer about the examples directory. I moved the code examples to there. I also decided to delete one of the py_safe_call_object_once examples, leaving only the version with an atomic flag because I think that's a useful pattern to document.

ngoldbaum and others added 2 commits March 23, 2026 15:52
Co-authored-by: da-woods <dw-git@d-woods.co.uk>
@ngoldbaum
Copy link
Copy Markdown
Contributor Author

OK, I think tests are passing again. Anything else needed here?

@da-woods da-woods enabled auto-merge (squash) March 24, 2026 07:28
@da-woods da-woods merged commit 87d8efc into cython:master Mar 24, 2026
77 checks passed
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.

3 participants