-
Notifications
You must be signed in to change notification settings - Fork 51
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
Support Free Threading in Python 3.13 #608
Comments
Various changes:
For the framework bindings there is in general no global state to protected (other than the ObjC runtime, which is thread safe and is accessed through pyobjc-core). There are a number of frameworks with extension contexts for which the implementation uses python tuples to store information. Those should be as safe without the GIL as with the GIL because the context info is owned by whatever object performs the callback. There is a very small risk at crashes when the owning object gets released, that's unchanged with and without the GIL and I'm not yet convinced that there's a window for a race condition in the first place. |
…freethreaded python Issue #608
Primary reason is that ownership of the result of PySequence_Fast_GET_ITEM is a borrowed reference that may be in a list. That's problematic in regular builds, and even more so in free threaded builds because the list can be mutated while using the item. Issue #608
Still far removed from actually supporting GIL-less operations. Issue #608
Check all extensions in a the various framework bindings for safety in a free threaded build, and add locking where necessary. Issue #608
With changeset 5058104 the extension modules in the various framework bindings should be thread safe even in free-threading mode. That's still only a first step toward supporting that mode, the core bridge still needs to be converted and that's more involved that the fairly trivial extension modules in framework bindings. |
This also audits the module for free-threaded use, which required using PyDict_GetItemRef. That's the cause for updating the various "pyobjc-compat.h" files: Add a backward compat implementation for older python versions. Issue #608
Note that the extension is not (yet) compatible with free threading. Issue #608
All of them should also be safe for use with free threading (with some ad-hoc atomics to protect global counters). Issue #608
Switch to the function API, and in particular to PyList_GetItemRef instead of PyList_GET_ITEM because the latter is problematic in free threaded mode. Issue #608
First steps in migrating to PyDict_GetItemRef for fetching items from a dict because the older APIs return a borrowed reference. Also stop using "PyDict.*String" APIs, explicitly construct a string object where needed and use *PyObjCNM* constants when using constant strings. The changeset also marks unwanted APIs as unavailable in "python-api-used.h" to ensure that there will be compile errors when building using the static analyser. Finally disable optimization for now to easy debugging while reworking pyobjc-core (will be renabled later). Issue #608
There's a couple of other calls to PyDict_GetItem left, those are in functions that return the borrowed reference and need further restructuring to switch to returning a new reference. Issue #608
Also drop usage of PyDict_SetItemString. Issue #608
This enabled switching to PyDict_GetItemRef in that function. Issue #608.
Also dropped usage of |
The only calls to PyDict_GetItem left are those in a couple of functions using borrowed references. Issue #608.
Also adds os.PathLike support to the FSRef constructor. Issue #608
These are updates based on the audit of a number of source files in pyobjc-core. Slowly getting closer to the parts of the implementation that need more design work w.r.t. locking. Issue #608
PySequence_Fast has semantics is not ideal in general, and more so in free threaded mode: when the argument is a python list it is returned as is, which means the borrowed references returned from ..._GET_ITEM are problematic due to concurrent modification of the list. Switch to a new ``PyObjCSequence_Tuple`` API that is a version of ``PySequence_Tuple`` with a message that gives more context to th user. The error raised by ``PySequene_Tuple`` is used as the ``__cause__`` of the exception raised by the new API. Issue #608
Thanks for working on this. I'm eager for there to be something that builds and installs, even if it crashes or is unstable when it runs. I've opted into Python 3.13t as my main Python, including my shell and test runners, etc, but I've had to fall back to another Python in many cases because pyobjc simply fails to build. In many cases, the dependency is unused, but its presence blocks the depending project on installing. Installing from source, I'm also affected by #620, so it's possible once that's addressed, I'll be able to build from source and get an unstable install. I share all of this just so you're aware of my situation. Unless there's something trivially easy you can do, I'm not looking for any support or changes. I'm mainly aiming to share the status from my perspective. I appreciate your work. |
I'll push out a release for #620 soonish, but other than that I won't release wheels for the free threaded build of Python until I'm somewhat sure that pyobjc-core actually supports running without the GIL. I'm getting closer to that, but need to finish work on a locking strategy and internal API for the two mappings between original objects and their proxy (from Python to ObjC and v.v.). Those mappings are needed to preserve identity, which is necessary for correctness. The hardest part at this point is finding the time to actually do the work (famous last words...). |
Issue #608 Slowly getting closer to the core bridge being ready to enable GIL-less operation.
FYI: The upcoming 10.3.2 release will include wheels for the free threaded variant of Python, but will still require the GIL. Even this required changes to the code due to using the limited API in framework bindings. |
This changeset does two things: 1. Add locking to the functions for updating and quering the registry (for the free-threaded build only) 2. Switch the mapping from Python to Objective-C proxies to using weak values for the Objective-C proxies. That makes clearing the mapping thread safe for both build variants, and allows removing the implementation of ``-release`` in the ``OC_Python*`` classes that acquired the GIL to remove a race condition between the invocation of ``-dealloc`` in a just released Objective-C value and removing that value from the mapping in a different thread.
Big step towards finishing this work: the proxy-registry (mapping from a Python object to its Objective-C proxy, and mapping an Objective-C object to its Python proxy) is now compatible with free-threading. (Still 19 files to go for the audit) |
OC_Python* classes intentionally do not use locking for accessing their state. That's primarily because these values are immutable. There's a small window for seeing incomplete values in the |
The various OC_Python* classes are now compatible with free threading, most did not require changes because the proxy objects themself are immutable (e.g. the fields of an OC_PythonArray don't change after initialization, even if the list is a proxy for is mutated)
this changeset makes updating the option values thread safe in a free-threaded build. Also switch the usage of PyObjC_Encoder and PyObjC_Decoder values to a safe pattern for free-threading. This also simplifies those usages (e.g. a win regardless of free-threading). Finally... the default value for PyObject* options is now Py_None instead of NULL, which can simplify there usage (although I haven't touched those yet).
Keyvalue coding support is now compaible with free threading
The various type checking options are now safe in the free threaded build.
The usage of these options is now compatible with free threading.
All usage of options.m is now compatible with free-threading.
"module.m" is now compatible with free-threading.
This is a first step in making objc-object.m safe to use in a free-threaded build. The change to method-accessor.m fixes a real bug: a PyObjCClass instance was used as a PyObjCObject one. This happens to work due to both having the Objective-C value as their first field, but is undefined behaviour in C.
objc-object is now almost ready for free-threading, with the exception of usage of '->flags': I haven't audited the code yet to see if that's safe or needs more work (most flags are static, but not all). The rest of the code is not 100% safe with free-threading, but the safeness issues should also be present in when using the GIL: calling an 'init' method on an UNINITIALIZED value in two threads at once has a race condition where one of the threads uses a no longer valid value. That's close to impossible to guard against.
The most impacting changes: - Internal API no longer uses borrowed references - The internal registry was reworked to make it thread-safe Also changes ``#if Py_GIL_DISABLED`` to ``#ifdef Py_GIL_DISABLED`` in a number of places.
This changeset fixes a number of small issues in the free-threading support, and enables GIL-less operation by default. This is the first point where tests in pyobjc-core pass with the free-threaded build. Next up: actually testing threaded usage.
Gettting closer, but not quite there yet a fairly trivial script using threading fails without the GIL and passes with it enabled (basically iterating over an NSArray instance concurrently in a couple of threads). I've added some standalone test script that demonstrate problems. |
There are not integrated in the main test suite because that test suite can hide problems by way of doing a lot of work before ending up at these tests.
The logic in objc_metaclass_register was off, resulting in a race condition when using PyObjC in multiple threads concurrently. The two test scripts in Tools/free-threading now work properly.
Those problems are now fixed, feeling better about this... |
Hmmm.... The overhead for free-threading is large enough to slow things down a lot when running without a GIL. That said, when I don't convert the list of an NSArray I get a similar slowdown)
The script: import threading
from Cocoa import NSObject, NSArray
import objc
N_THREAD=8
v = NSObject.alloc().init()
t_list = []
r_list = []
def f(a, b):
b.wait()
s = 0
for i in a:
s += i
bar = threading.Barrier(N_THREAD)
arr = NSArray.arrayWithArray_(list(range(1000000)))
for _ in range(N_THREAD):
t = threading.Thread(target=f, args=(arr, bar,))
t_list.append(t)
t.start()
for t in t_list:
t.join() |
Removing ObjC proxies from the registry was broken due to the switch to weak references.
Is your feature request related to a problem? Please describe.
Python 3.13 introduces an (experimental) free threading option (aka "NO_GIL"). PyObjC should support this feature.
Describe the solution you'd like
TL/DR: This is a lot more work than "build extension modules using a free threaded build"
Adding this requires a number of changes to PyObjC and the build process:
This one and the previous one are implemented by updating
_common_definitions.py
The hard part is updating pyobjc-core, that code relies on module global state that's protected by the GIL. A somewhat easy option is to switch to a "global pyobjc lock" in the short term and slowly peel of bits that can use more fine grained locking.
See also:
The text was updated successfully, but these errors were encountered: