Giter Club home page Giter Club logo

tkthread's Introduction

tkthread

Easy multithreading with Tkinter on CPython 2.7/3.x and PyPy 2.7/3.x.

import tkthread; tkthread.patch()   # do this before importing tkinter

Background

The Tcl/Tk language that comes with Python follows a different threading model than Python itself which can raise obtuse errors when mixing Python threads with Tkinter, such as:

RuntimeError: main thread is not in main loop
RuntimeError: Calling Tcl from different apartment
NotImplementedError: Call from another thread

Tcl can have many isolated interpreters, and each are tagged to the its particular OS thread when created. Python's _tkinter module checks if the calling Python thread is different than the Tcl/Tk thread, and if so, waits one second for the Tcl/Tk main loop to begin dispatching. If there is a timeout, a RuntimeError is raised. On PyPy, a NotImplementedError is raised.

For non-Tk calls into Tcl, Python will raise an apartment RuntimeError when calling a Tcl interpreter from a different thread.

A common approach to avoid these errors involves using .after to set up periodic polling of a message queue from the Tcl/Tk main loop, which can slow the responsiveness of the GUI.

The initial approach used in tkthread is to use the Tcl/Tk thread::send messaging to notify the Tcl/Tk main loop of a call for execution. This interrupt-style architecture has lower latency and better CPU utilization than periodic polling. This works with CPython and PyPy.

The newer approach used in tkthread is to use tkthread.tkinstall() to patch Tkinter when make calls into Tcl/Tk. This only works on CPython and it does not require the Thread package in Tcl.

Usage on CPython (simplest)

For CPython 2.7/3.x, tkthread.patch() (same as tkthread.tkinstall()) can be called first, and will patch Tkinter to re-route threaded calls to the Tcl interpreter using the willdispatch internal API call.

import tkthread; tkthread.patch()
import tkinter as tk

root = tk.Tk()

import threading
def thread_run(func): threading.Thread(target=func).start()

@thread_run
def func():
    root.wm_title(threading.current_thread())

    @tkthread.main(root)
    @tkthread.current(root)
    def testfunc():
        tk.Label(text=threading.current_thread()).pack()

root.mainloop()

Usage on CPython/PyPy (compatibility/legacy)

The tkthread module provides the TkThread class, which can synchronously interact with the main thread.

from tkthread import tk, TkThread

root = tk.Tk()        # create the root window
tkt = TkThread(root)  # make the thread-safe callable

import threading, time
def run(func):
    threading.Thread(target=func).start()

run(lambda:     root.wm_title('FAILURE'))
run(lambda: tkt(root.wm_title,'SUCCESS'))

root.update()
time.sleep(2)  # _tkinter.c:WaitForMainloop fails
root.mainloop()

The tkt instance is callable, and will wait for the Tcl/Tk main loop to execute and compute a result which is then passed back for return in the calling thread. A non-synchronous version also exists that does not block:

tkt.nosync(root.wm_title, 'ALSO SUCCESS')

There is an optional tkt.install() method which intercepts Python-to-Tk calls. This must be called on the default root, before the creation of child widgets. If installed, then wrapping Tk widget calls in threaded code with tkt is not necessary. There is, however, a slight performance penalty for Tkinter widgets that operate only on the main thread because of the thread-checking indirection.

The root Tcl/Tk interpreter must be the primary interpreter on the main thread. If it is not, then you will receive a TclError of the form:

_tkinter.TclError: invalid command name "140520536224520_call_from"

For example, creating several Tk() instances and then using TkThread on those will cause this error.

A good practice is to create a root window and then call root.withdraw() to keep the primary Tcl/Tk interpreter active. Future Toplevel windows use root as the master.

Install

pip install tkthread

API

  • tkthread.patch()
    • patch Tkinter to support multi-threading.
  • tkthread.tkinstall()
    • same as .patch()
  • tkthread.call(func, *args, **kw)
    • call func on the main thread, with arguments and keyword arguments.
    • waits for return value (or raises error)
  • tkthread.call_nosync(func, *args, **kw)
    • call func on the main thread, with arguments and keyword arguments.
    • returns immediately, ignore return func return or error.
  • @tkthread.called_on_main
    • decorator to dispatch the function call on the main thread from the calling thread.
  • @tkthread.main()
    • decorator to call a function immediately on the main thread.
  • @tkthread.current()
    • decorator to call a function immediately on the current thread.
  • TkThread(root)
    • class to dispatch thread calls to Tk using thread::send

Known (and solved) Error Messages

You may receive this error when using tkthread.TkThread(root):

_tkinter.TclError: can't find package Thread

This means that Python's Tcl/Tk libraries do not include the Thread package, which is needed by TkThread.

On Debian/Ubuntu:

apt install tcl-thread

On Windows, you'll need to manually update your Tcl installation to include the Thread package.

The simpler solution is to use tkthread.patch() instead.

When using Matplotlib, you may receive a warning message that can be ignored:

UserWarning: Starting a Matplotlib GUI outside of the main thread will likely fail.

The demo/mpl_plot.py script shows an example with this message.

License

Licensed under the Apache License, Version 2.0 (the "License")

See Also

These libraries offer similar functionality, using periodic polling:

tkthread's People

Contributors

serwy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

tkthread's Issues

_tkinter.TclError: can't find package Thread

I assume your package would help me a lot, as I try to find/emulate something like an "invokeLater" or "runOnUIThread" for Tkinter in python3 that works on both pypy and cpython (best - that works on any implementation). After installation on pypy3.6.7.1.1 (installation itself worked) I got

$ pypy y.py
Traceback (most recent call last):
  File "y.py", line 4, in <module>
    tkt = TkThread(root)  # make the thread-safe callable
  File "/opt/pypy3.6-7.1.1-beta-linux_x86_64-portable/site-packages/tkthread/__init__.py", line 120, in __init__
    self.root.eval('package require Thread')
  File "/opt/pypy3.6-7.1.1-beta-linux_x86_64-portable/lib_pypy/_tkinter/app.py", line 344, in eval
    self.raiseTclError()
  File "/opt/pypy3.6-7.1.1-beta-linux_x86_64-portable/lib_pypy/_tkinter/app.py", line 168, in raiseTclError
    tklib.Tcl_GetStringResult(self.interp)).decode('utf-8'))
_tkinter.TclError: can't find package Thread

where y.py is the sample on this packages homepage.

from tkthread import tk, TkThread

root = tk.Tk()        # create the root window
tkt = TkThread(root)  # make the thread-safe callable

import threading, time
def run(func):
    threading.Thread(target=func).start()

run(lambda:     root.wm_title('FAILURE'))
run(lambda: tkt(root.wm_title,'SUCCESS'))

root.update()
time.sleep(2)  # _tkinter.c:WaitForMainloop fails
root.mainloop()

I am on Ubuntu 18.04 and have tk-dev installed

 $ dpkg -l | grep tk8
ii  libtk8.6:amd64                         8.6.8-4                                amd64        Tk toolkit for Tcl and X11 v8.6 - run-time files
ii  tk8.6                                  8.6.8-4                                amd64        Tk toolkit for Tcl and X11 v8.6 - windowing shell
ii  tk8.6-blt2.5                           2.5.3+dfsg-4                           amd64        graphics extension library for Tcl/Tk - library
ii  tk8.6-dev:amd64                        8.6.8-4                                amd64        Tk toolkit for Tcl and X11 v8.6 - development files
$ uname -a
Linux x220t 4.15.0-58-generic #64-Ubuntu SMP Tue Aug 6 11:12:41 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.3 LTS"

Any help appreciated.

ModuleNotFoundError: No module named 'tkthread'

No idea if this is something I've done wrong or not but I'm getting the aforementioned error. I tried pip install tkthread and installing the github repo with pip as well. When I run pip freeze it pops up fine as tkthread==0.5.2.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.