Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions Doc/library/tkinter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
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 thread support. The official Python binary release bundles Tcl/Tk 8.6
Tkinter supports a range of Tcl/Tk versions, which must be built with
thread support. The official Python binary release bundles Tcl/Tk 8.6
threaded. See the source code for the :mod:`_tkinter` module
for more information about supported versions.

Expand Down Expand Up @@ -534,16 +534,11 @@

A number of special cases exist:

* Tcl/Tk libraries can be built so they are not thread-aware. In this case,
: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`

Check warning on line 537 in Doc/library/tkinter.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:class reference target not found: Tk [ref.class]

Check warning on line 537 in Doc/library/tkinter.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:class reference target not found: Tk [ref.class]
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
132 changes: 50 additions & 82 deletions Modules/_tkinter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -172,16 +173,11 @@ _get_tcl_lib_path(void)
}
#endif /* MS_WINDOWS */

/* The threading situation is complicated. Tcl is not thread-safe, except
when configured with --enable-threads.
/* The threading situation is complicated.
We require that Tcl is compiled for 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
Expand Down Expand Up @@ -213,9 +209,8 @@ _get_tcl_lib_path(void)
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
Expand All @@ -225,49 +220,54 @@ _get_tcl_lib_path(void)
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;
#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

#ifdef TCL_THREADS
static Tcl_ThreadDataKey state_key;
typedef PyThreadState *ThreadSpecificData;
#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
Expand All @@ -282,7 +282,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;
Expand All @@ -307,7 +306,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;
Expand Down Expand Up @@ -575,26 +574,10 @@ Tkapp_New(const char *screenName, const char *className,

v->interp = Tcl_CreateInterp();
v->wantobjects = wantobjects;
v->threaded = Tcl_GetVar2Ex(v->interp, "tcl_platform", "threaded",
TCL_GLOBAL_ONLY) != NULL;
v->thread_id = Tcl_GetCurrentThread();
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;
Expand Down Expand Up @@ -1442,13 +1425,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. */
Expand All @@ -1469,7 +1450,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;
Expand Down Expand Up @@ -1746,7 +1727,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;
Expand Down Expand Up @@ -1780,7 +1761,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);
}

Expand Down Expand Up @@ -2438,7 +2419,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;

Expand All @@ -2450,7 +2431,7 @@ _tkinter_tkapp_createcommand_impl(TkappObject *self, const char *name,
Py_INCREF(self);
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;
Expand Down Expand Up @@ -2507,7 +2488,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;
Expand Down Expand Up @@ -2836,8 +2817,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;

Expand All @@ -2848,23 +2827,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;
Expand Down Expand Up @@ -3361,13 +3327,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
Expand Down Expand Up @@ -3452,9 +3416,13 @@ PyInit__tkinter(void)
{
PyObject *m, *uexe, *cexe;

tcl_lock = PyThread_allocate_lock();
if (tcl_lock == NULL)
return NULL;
#if TCL_MAJOR_VERSION < 9 /* Tcl 9.x is always threaded */
if (_check_tcl_threaded() == 0) {
PyErr_SetString(PyExc_ImportError,
"Tcl must be compiled with thread support");
return 0;
}
#endif

m = PyModule_Create(&_tkintermodule);
if (m == NULL)
Expand Down
Loading