From 53a3669dd712c54e05bae71149c67c5506d2e80a Mon Sep 17 00:00:00 2001 From: Michiel Jan Laurens de Hoon Date: Sat, 8 Nov 2025 22:10:08 +0900 Subject: [PATCH 1/7] require that Tcl is built with thread support --- Doc/library/tkinter.rst | 11 +--- Modules/_tkinter.c | 115 ++++++++++++++-------------------------- 2 files changed, 42 insertions(+), 84 deletions(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 44f688ad0d6422..d615955667b270 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -17,7 +17,7 @@ demonstrating a simple Tk interface, letting you know that :mod:`!tkinter` is properly installed on your system, and also showing what version of Tcl/Tk is installed, so you can read the Tcl/Tk documentation specific to that version. -Tkinter supports a range of Tcl/Tk versions, built either with or without +Tkinter supports a range of Tcl/Tk versions, which must be built with thread support. Tcl/Tk 8.5.12 is the minimum supported version; the official Python binary release bundles Tcl/Tk 9.0. @@ -476,18 +476,11 @@ interpreter will fail. A number of special cases exist: -* Tcl/Tk libraries built without thread support are now rare: Tcl/Tk 9.0 (the - bundled version) is always thread-aware, so this case only arises with some - older 8.x builds. When the library is not thread-aware, - :mod:`!tkinter` calls the library from the originating Python thread, even - if this is different than the thread that created the Tcl interpreter. A global - lock ensures only one call occurs at a time. - * While :mod:`!tkinter` allows you to create more than one instance of a :class:`Tk` object (with its own interpreter), all interpreters that are part of the same thread share a common event queue, which gets ugly fast. In practice, don't create more than one instance of :class:`Tk` at a time. Otherwise, it's best to create - them in separate threads and ensure you're running a thread-aware Tcl/Tk build. + them in separate threads. * Blocking event handlers are not the only way to prevent the Tcl interpreter from reentering the event loop. It is even possible to run multiple nested event loops diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index f5fb1841f0d594..5903b796622da5 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -40,9 +40,10 @@ Copyright (C) 1994 Steen Lumholt. #define CHECK_SIZE(size, elemsize) \ ((size_t)(size) <= Py_MIN((size_t)INT_MAX, UINT_MAX / (size_t)(elemsize))) -/* If Tcl is compiled for threads, we must also define TCL_THREAD. We define - it always; if Tcl is not threaded, the thread functions in - Tcl are empty. */ +/* As we require that Tcl is compiled for threads, we must also define + TCL_THREADS. We define it always; if Tcl is not threaded, the thread + functions in Tcl are empty. We check if Tcl is actually compiled for + threads when importing this module. */ #define TCL_THREADS #ifdef TK_FRAMEWORK @@ -179,6 +180,7 @@ _get_tcl_lib_path(void) } #endif /* MS_WINDOWS */ +<<<<<<< HEAD #if defined(MS_WINDOWS) && TK_MAJOR_VERSION >= 9 static void mount_tk_dll_zip(void) @@ -233,13 +235,8 @@ Tkinter_TkInit(Tcl_Interp *interp) /* The threading situation is complicated. Tcl is not thread-safe, except when configured with --enable-threads. - So we need to use a lock around all uses of Tcl. Previously, the - Python interpreter lock was used for this. However, this causes - problems when other Python threads need to run while Tcl is blocked - waiting for events. - - To solve this problem, a separate lock for Tcl is introduced. - Holding it is incompatible with holding Python's interpreter lock. + We introduce a lock specifically for Tcl; holding it is incompatible + with holding Python's interpreter lock. The following four macros manipulate both locks together. ENTER_TCL and LEAVE_TCL are brackets, just like @@ -271,9 +268,8 @@ Tkinter_TkInit(Tcl_Interp *interp) These locks expand to several statements and brackets; they should not be used in branches of if statements and the like. - If Tcl is threaded, this approach won't work anymore. The Tcl - interpreter is only valid in the thread that created it, and all Tk - activity must happen in this thread, also. That means that the + The Tcl interpreter is only valid in the thread that created it, and + all Tk activity must happen in this thread, also. That means that the mainloop must be invoked in the thread that created the interpreter. Invoking commands from other threads is possible; _tkinter will queue an event for the interpreter thread, which will @@ -283,48 +279,51 @@ Tkinter_TkInit(Tcl_Interp *interp) the command invocation will block. In addition, for a threaded Tcl, a single global tcl_tstate won't - be sufficient anymore, since multiple Tcl interpreters may - simultaneously dispatch in different threads. So we use the Tcl TLS - API. + be sufficient, since multiple Tcl interpreters may simultaneously + dispatch in different threads. So we use the Tcl TLS API. */ -static PyThread_type_lock tcl_lock = 0; +static int +_check_tcl_threaded(void) +{ + Tcl_Interp* interp; + Tcl_Obj* threaded; + interp = Tcl_CreateInterp(); + threaded = Tcl_GetVar2Ex(interp, + "tcl_platform", + "threaded", + TCL_GLOBAL_ONLY); + Tcl_DeleteInterp(interp); + if (threaded == NULL) return 0; + else return 1; +} -#ifdef TCL_THREADS static Tcl_ThreadDataKey state_key; #define tcl_tstate \ (*(PyThreadState**)Tcl_GetThreadData(&state_key, sizeof(PyThreadState*))) -#else -static PyThreadState *tcl_tstate = NULL; -#endif #define ENTER_TCL \ { PyThreadState *tstate = PyThreadState_Get(); \ Py_BEGIN_ALLOW_THREADS \ - if(tcl_lock)PyThread_acquire_lock(tcl_lock, 1); \ tcl_tstate = tstate; #define LEAVE_TCL \ tcl_tstate = NULL; \ - if(tcl_lock)PyThread_release_lock(tcl_lock); \ Py_END_ALLOW_THREADS} #define ENTER_OVERLAP \ Py_END_ALLOW_THREADS #define LEAVE_OVERLAP_TCL \ - tcl_tstate = NULL; if(tcl_lock)PyThread_release_lock(tcl_lock); } + tcl_tstate = NULL; } #define ENTER_PYTHON \ { PyThreadState *tstate = tcl_tstate; tcl_tstate = NULL; \ - if(tcl_lock) \ - PyThread_release_lock(tcl_lock); \ PyEval_RestoreThread((tstate)); } #define LEAVE_PYTHON \ { PyThreadState *tstate = PyEval_SaveThread(); \ - if(tcl_lock)PyThread_acquire_lock(tcl_lock, 1); \ tcl_tstate = tstate; } #ifndef FREECAST @@ -339,7 +338,6 @@ typedef struct { PyObject_HEAD Tcl_Interp *interp; int wantobjects; - int threaded; /* True if tcl_platform[threaded] */ Tcl_ThreadId thread_id; int dispatching; PyObject *trace; @@ -373,7 +371,7 @@ typedef struct { static inline int check_tcl_appartment(TkappObject *app) { - if (app->threaded && app->thread_id != Tcl_GetCurrentThread()) { + if (app->thread_id != Tcl_GetCurrentThread()) { PyErr_SetString(PyExc_RuntimeError, "Calling Tcl from different apartment"); return -1; @@ -652,20 +650,6 @@ Tkapp_New(const char *screenName, const char *className, v->dispatching = 0; v->trace = NULL; -#ifndef TCL_THREADS - if (v->threaded) { - PyErr_SetString(PyExc_RuntimeError, - "Tcl is threaded but _tkinter is not"); - Py_DECREF(v); - return 0; - } -#endif - if (v->threaded && tcl_lock) { - /* If Tcl is threaded, we don't need the lock. */ - PyThread_free_lock(tcl_lock); - tcl_lock = NULL; - } - v->OldBooleanType = Tcl_GetObjType("boolean"); { Tcl_Obj *value; @@ -1659,13 +1643,11 @@ Tkapp_CallProc(Tcl_Event *evPtr, int flags) /* This is the main entry point for calling a Tcl command. - It supports three cases, with regard to threading: - 1. Tcl is not threaded: Must have the Tcl lock, then can invoke command in - the context of the calling thread. - 2. Tcl is threaded, caller of the command is in the interpreter thread: + It supports two cases, with regard to threading: + 2. Caller of the command is in the interpreter thread: Execute the command in the calling thread. Since the Tcl lock will not be used, we can merge that with case 1. - 3. Tcl is threaded, caller is in a different thread: Must queue an event to + 3. Caller is in a different thread: Must queue an event to the interpreter thread. Allocation of Tcl objects needs to occur in the interpreter thread, so we ship the PyObject* args to the target thread, and perform processing there. */ @@ -1686,7 +1668,7 @@ Tkapp_Call(PyObject *selfptr, PyObject *args) if (PyTuple_Check(item)) args = item; } - if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) { + if (self->thread_id != Tcl_GetCurrentThread()) { /* We cannot call the command directly. Instead, we must marshal the parameters to the interpreter thread. */ Tkapp_CallEvent *ev; @@ -1963,7 +1945,7 @@ static PyObject* var_invoke(EventFunc func, PyObject *selfptr, PyObject *args, int flags) { TkappObject *self = TkappObject_CAST(selfptr); - if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) { + if (self->thread_id != Tcl_GetCurrentThread()) { VarEvent *ev; // init 'res' and 'exc' to make static analyzers happy PyObject *res = NULL, *exc = NULL; @@ -1997,7 +1979,7 @@ var_invoke(EventFunc func, PyObject *selfptr, PyObject *args, int flags) } return res; } - /* Tcl is not threaded, or this is the interpreter thread. */ + /* This is the interpreter thread. */ return func(self, args, flags); } @@ -2655,7 +2637,7 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name, return NULL; } - if (self->threaded && self->thread_id != Tcl_GetCurrentThread() && + if (self->thread_id != Tcl_GetCurrentThread() && !WaitForMainloop(self)) return NULL; @@ -2669,7 +2651,7 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name, (gh-80937). The command cannot outlive the interpreter. */ data->self = self; data->func = Py_NewRef(func); - if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) { + if (self->thread_id != Tcl_GetCurrentThread()) { err = 0; // init to make static analyzers happy Tcl_Condition cond = NULL; @@ -2727,7 +2709,7 @@ _tkinter_tkapp_deletecommand_impl(TkappObject *self, const char *name) TRACE(self, ("((sss))", "rename", name, "")); - if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) { + if (self->thread_id != Tcl_GetCurrentThread()) { err = 0; // init to make static analyzers happy Tcl_Condition cond = NULL; @@ -3082,8 +3064,6 @@ static PyObject * _tkinter_tkapp_mainloop_impl(TkappObject *self, int threshold) /*[clinic end generated code: output=0ba8eabbe57841b0 input=036bcdcf03d5eca0]*/ { - PyThreadState *tstate = PyThreadState_Get(); - CHECK_TCL_APPARTMENT(self); self->dispatching = 1; @@ -3094,23 +3074,10 @@ _tkinter_tkapp_mainloop_impl(TkappObject *self, int threshold) { int result; - if (self->threaded) { - /* Allow other Python threads to run. */ - ENTER_TCL - result = Tcl_DoOneEvent(0); - LEAVE_TCL - } - else { - Py_BEGIN_ALLOW_THREADS - if(tcl_lock)PyThread_acquire_lock(tcl_lock, 1); - tcl_tstate = tstate; - result = Tcl_DoOneEvent(TCL_DONT_WAIT); - tcl_tstate = NULL; - if(tcl_lock)PyThread_release_lock(tcl_lock); - if (result == 0) - Sleep(Tkinter_busywaitinterval); - Py_END_ALLOW_THREADS - } + /* Allow other Python threads to run. */ + ENTER_TCL + result = Tcl_DoOneEvent(0); + LEAVE_TCL if (PyErr_CheckSignals() != 0) { self->dispatching = 0; @@ -3686,13 +3653,11 @@ EventHook(void) } #endif Py_BEGIN_ALLOW_THREADS - if(tcl_lock)PyThread_acquire_lock(tcl_lock, 1); tcl_tstate = event_tstate; result = Tcl_DoOneEvent(TCL_DONT_WAIT); tcl_tstate = NULL; - if(tcl_lock)PyThread_release_lock(tcl_lock); if (result == 0) Sleep(Tkinter_busywaitinterval); Py_END_ALLOW_THREADS From 8d199d47855dad90a3828f13f582ac2b2e2a62c3 Mon Sep 17 00:00:00 2001 From: Michiel Jan Laurens de Hoon Date: Sat, 8 Nov 2025 23:29:55 +0900 Subject: [PATCH 2/7] don't check for thread support on Tcl 9.x, as it is always threaded and the tcl_platform(threaded) variable has been removed --- Modules/_tkinter.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 5903b796622da5..3dd55f3f56a199 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -180,7 +180,6 @@ _get_tcl_lib_path(void) } #endif /* MS_WINDOWS */ -<<<<<<< HEAD #if defined(MS_WINDOWS) && TK_MAJOR_VERSION >= 9 static void mount_tk_dll_zip(void) @@ -284,6 +283,7 @@ Tkinter_TkInit(Tcl_Interp *interp) */ +#if TCL_MAJOR_VERSION < 9 /* Tcl 9.x is always threaded */ static int _check_tcl_threaded(void) { @@ -298,6 +298,7 @@ _check_tcl_threaded(void) if (threaded == NULL) return 0; else return 1; } +#endif static Tcl_ThreadDataKey state_key; #define tcl_tstate \ @@ -3752,10 +3753,6 @@ PyInit__tkinter(void) PyObject *m, *uexe, *cexe; - tcl_lock = PyThread_allocate_lock(); - if (tcl_lock == NULL) - return PyErr_NoMemory(); - m = PyModule_Create(&_tkintermodule); if (m == NULL) return NULL; From da2785dffdcf7cc8a6dd1a7ba781e22fa2586b99 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 8 Nov 2025 13:22:34 +0000 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-11-08-13-22-33.gh-issue-140494.L_cKec.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-11-08-13-22-33.gh-issue-140494.L_cKec.rst diff --git a/Misc/NEWS.d/next/Library/2025-11-08-13-22-33.gh-issue-140494.L_cKec.rst b/Misc/NEWS.d/next/Library/2025-11-08-13-22-33.gh-issue-140494.L_cKec.rst new file mode 100644 index 00000000000000..784be0aa0492d0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-08-13-22-33.gh-issue-140494.L_cKec.rst @@ -0,0 +1,2 @@ +Require that Tcl is always built with thread support. +This requirement only affects Tcl 8.x, as Tcl 9.x is always thread-enabled. From f73ddd264071674078ce0ef3786871a67790544f Mon Sep 17 00:00:00 2001 From: Michiel Jan Laurens de Hoon Date: Sat, 18 Jul 2026 10:32:17 +0900 Subject: [PATCH 4/7] Check if Tcl is threaded when creating a tkapp --- ...-11-08-13-22-33.gh-issue-140494.L_cKec.rst | 2 +- Modules/_tkinter.c | 43 +++++++++---------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-11-08-13-22-33.gh-issue-140494.L_cKec.rst b/Misc/NEWS.d/next/Library/2025-11-08-13-22-33.gh-issue-140494.L_cKec.rst index 784be0aa0492d0..142cdac28f70f8 100644 --- a/Misc/NEWS.d/next/Library/2025-11-08-13-22-33.gh-issue-140494.L_cKec.rst +++ b/Misc/NEWS.d/next/Library/2025-11-08-13-22-33.gh-issue-140494.L_cKec.rst @@ -1,2 +1,2 @@ -Require that Tcl is always built with thread support. +Require that Tcl is always built with thread support. This requirement only affects Tcl 8.x, as Tcl 9.x is always thread-enabled. diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 3dd55f3f56a199..fa4261c787e85d 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -43,7 +43,7 @@ Copyright (C) 1994 Steen Lumholt. /* As we require that Tcl is compiled for threads, we must also define TCL_THREADS. We define it always; if Tcl is not threaded, the thread functions in Tcl are empty. We check if Tcl is actually compiled for - threads when importing this module. */ + threads when creating a tkapp. */ #define TCL_THREADS #ifdef TK_FRAMEWORK @@ -283,23 +283,6 @@ Tkinter_TkInit(Tcl_Interp *interp) */ -#if TCL_MAJOR_VERSION < 9 /* Tcl 9.x is always threaded */ -static int -_check_tcl_threaded(void) -{ - Tcl_Interp* interp; - Tcl_Obj* threaded; - interp = Tcl_CreateInterp(); - threaded = Tcl_GetVar2Ex(interp, - "tcl_platform", - "threaded", - TCL_GLOBAL_ONLY); - Tcl_DeleteInterp(interp); - if (threaded == NULL) return 0; - else return 1; -} -#endif - static Tcl_ThreadDataKey state_key; #define tcl_tstate \ (*(PyThreadState**)Tcl_GetThreadData(&state_key, sizeof(PyThreadState*))) @@ -633,6 +616,9 @@ Tkapp_New(const char *screenName, const char *className, { TkappObject *v; char *argv0; +#if TCL_MAJOR_VERSION < 9 /* Tcl 9.x is always threaded */ + static Tcl_Obj* threaded = NULL; +#endif PyTypeObject *tp = (PyTypeObject *)Tkapp_Type; v = (TkappObject *)tp->tp_alloc(tp, 0); @@ -640,6 +626,20 @@ Tkapp_New(const char *screenName, const char *className, return NULL; v->interp = Tcl_CreateInterp(); +#if TCL_MAJOR_VERSION < 9 /* Tcl 9.x is always threaded */ + if (threaded == NULL) { + threaded = Tcl_GetVar2Ex(v->interp, + "tcl_platform", + "threaded", + TCL_GLOBAL_ONLY); + if (threaded == NULL) { + PyErr_SetString(PyExc_RuntimeError, + "Tcl must be compiled with thread support"); + Py_DECREF(v); + return NULL; + } + } +#endif v->wantobjects = wantobjects; #if TCL_MAJOR_VERSION >= 9 v->threaded = 1; @@ -1645,10 +1645,9 @@ Tkapp_CallProc(Tcl_Event *evPtr, int flags) /* This is the main entry point for calling a Tcl command. It supports two cases, with regard to threading: - 2. Caller of the command is in the interpreter thread: - Execute the command in the calling thread. Since the Tcl lock will - not be used, we can merge that with case 1. - 3. Caller is in a different thread: Must queue an event to + 1. Caller of the command is in the interpreter thread: + Execute the command in the calling thread. + 2. Caller is in a different thread: Must queue an event to the interpreter thread. Allocation of Tcl objects needs to occur in the interpreter thread, so we ship the PyObject* args to the target thread, and perform processing there. */ From 1cb7f4b3723dec14f147978b9d13e68f0e17a841 Mon Sep 17 00:00:00 2001 From: Michiel Jan Laurens de Hoon Date: Sat, 18 Jul 2026 12:08:55 +0900 Subject: [PATCH 5/7] remove remaining reference to threaded --- Modules/_tkinter.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index fa4261c787e85d..16bfa8876b3fc8 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -3276,7 +3276,7 @@ Tkapp_Dealloc(PyObject *op) TkappObject *self = (TkappObject *)op; PyTypeObject *tp = Py_TYPE(op); PyObject_GC_UnTrack(op); - if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) { + if (self->thread_id != Tcl_GetCurrentThread()) { /* Deleting the interpreter from another thread aborts the process ("Tcl_AsyncDelete: async handler deleted by the wrong thread"). Leak it instead (gh-83274). */ From 3e998fde9e5e37ec6301413eb9c28e4046e7d42c Mon Sep 17 00:00:00 2001 From: Michiel Jan Laurens de Hoon Date: Sat, 18 Jul 2026 12:29:36 +0900 Subject: [PATCH 6/7] remove static, as non-constant global variables are generally not supported in the CPython repo. --- Modules/_tkinter.c | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 16bfa8876b3fc8..6907519ec8f542 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -617,7 +617,7 @@ Tkapp_New(const char *screenName, const char *className, TkappObject *v; char *argv0; #if TCL_MAJOR_VERSION < 9 /* Tcl 9.x is always threaded */ - static Tcl_Obj* threaded = NULL; + Tcl_Obj* threaded; #endif PyTypeObject *tp = (PyTypeObject *)Tkapp_Type; @@ -627,17 +627,15 @@ Tkapp_New(const char *screenName, const char *className, v->interp = Tcl_CreateInterp(); #if TCL_MAJOR_VERSION < 9 /* Tcl 9.x is always threaded */ + threaded = Tcl_GetVar2Ex(v->interp, + "tcl_platform", + "threaded", + TCL_GLOBAL_ONLY); if (threaded == NULL) { - threaded = Tcl_GetVar2Ex(v->interp, - "tcl_platform", - "threaded", - TCL_GLOBAL_ONLY); - if (threaded == NULL) { - PyErr_SetString(PyExc_RuntimeError, - "Tcl must be compiled with thread support"); - Py_DECREF(v); - return NULL; - } + PyErr_SetString(PyExc_RuntimeError, + "Tcl must be compiled with thread support"); + Py_DECREF(v); + return NULL; } #endif v->wantobjects = wantobjects; From 6899a2c571f729846c065e30f20e4d4e7183f12a Mon Sep 17 00:00:00 2001 From: Michiel Jan Laurens de Hoon Date: Sat, 18 Jul 2026 13:12:26 +0900 Subject: [PATCH 7/7] remove v->threaded, as it's a local variable now --- Modules/_tkinter.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c index 6907519ec8f542..5c85041a97dc70 100644 --- a/Modules/_tkinter.c +++ b/Modules/_tkinter.c @@ -639,12 +639,6 @@ Tkapp_New(const char *screenName, const char *className, } #endif v->wantobjects = wantobjects; -#if TCL_MAJOR_VERSION >= 9 - v->threaded = 1; -#else - v->threaded = Tcl_GetVar2Ex(v->interp, "tcl_platform", "threaded", - TCL_GLOBAL_ONLY) != NULL; -#endif v->thread_id = Tcl_GetCurrentThread(); v->dispatching = 0; v->trace = NULL;