Concurrency¶
This page is all about running things concurrently in teek. That means doing something else while the GUI is running.
Threads¶
Here is some code that pastebins a Hello World to dpaste.com.
import requests
import teek
class Dpaster(teek.Frame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.paste_button = teek.Button(self, "Pastebin a hello world", self.paste)
self.paste_button.pack()
self.url_label = teek.Label(self)
self.url_label.pack()
def paste(self):
self.url_label.config['text'] = "Pasting..."
# api docs: http://dpaste.com/api/v2/
print("Starting to paste")
response = requests.post('http://dpaste.com/api/v2/',
data={'content': 'Hello World'})
response.raise_for_status()
url = response.text.strip()
print("Pasted successfully:", url)
self.url_label.config['text'] = url
window = teek.Window("dpaster")
Dpaster(window).pack()
window.geometry(250, 100)
window.on_delete_window.connect(teek.quit)
teek.run()
Run the program. If you click the pasting button, the whole GUI freezes for a couple seconds and the button looks like it’s pressed down, and when the pastebinning is done, everything is fine again. The freezing is not nice.
In this documentation, functions and methods that take a long time to complete
are called blocking. Our paste()
method is blocking, and using a
blocking function or method as a button click callback freezes things.
If you have code like this…
import time
def one_thing():
print("one thing")
time.sleep(1)
print("one thing")
time.sleep(1)
print("one thing")
def another_thing():
print("another thing")
time.sleep(1)
print("another thing")
time.sleep(1)
print("another thing")
one_thing()
another_thing()
…Python obviously doesn’t run one_thing()
and another_thing()
at the
same time; it’ll first run one_thing()
, and when it’s done, it’ll run
another_thing()
. However, if we add import threading
to the top of the
program and change the last 2 lines to this…
threading.Thread(target=one_thing).start()
another_thing()
…the functions will run at the same time.
Note
We are doing target=one_thing
, not target=one_thing()
. The
()
at the end of one_thing()
tell Python to run the function right
away, but instead of that, we want to run it in the thread.
The good *ehm* awesome news is that threads work nicely in teek.
Add import threading
to the top of the file, and add this method to the
Dpaster
class…
def start_pasting(self):
threading.Thread(target=self.paste).start()
…and change the line that creates self.paste_button
to this:
self.paste_button = teek.Button(self, "Pastebin a hello world", self.start_pasting)
Let’s try to run the program again. Clicking the button gives this error:
Traceback (most recent call last):
...
RuntimeError: init_threads() wasn't called
Let’s fix it by adding teek.init_threads()
before the line that creates
window
. All in all, the code looks like this now:
import threading
import requests
import teek
class Dpaster(teek.Frame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.paste_button = teek.Button(self, "Pastebin a hello world", self.start_pasting)
self.paste_button.pack()
self.url_label = teek.Label(self)
self.url_label.pack()
def start_pasting(self):
threading.Thread(target=self.paste).start()
def paste(self):
self.url_label.config['text'] = "Pasting..."
# api docs: http://dpaste.com/api/v2/
print("Starting to paste")
response = requests.post('http://dpaste.com/api/v2/',
data={'content': 'Hello World'})
response.raise_for_status()
url = response.text.strip()
print("Pasted successfully:", url)
self.url_label.config['text'] = url
teek.init_threads()
window = teek.Window("dpaster")
Dpaster(window).pack()
window.geometry(250, 100)
window.on_delete_window.connect(teek.quit)
teek.run()
Run the program. It works!
How does it work, and why did it freeze??¶
Note
This section assumes that you know event loop stuff.
Button click callbacks are ran in the event loop. If the button command takes
2 seconds or so to run, the event loop will run it for 2 seconds, but it can’t
do anything else while it’s doing that (see the event loop stuff link above to
understand why). However, start_pasting()
is not blocking, and running that
from the event loop is fine.
If you have used tkinter
before, the above program probably looks quite
wrong to you, because in tkinter, running anything like
self.url_label.config['text'] = "Pasting..."
in a thread is BAD.
Threads and tkinter don’t work together well, and you must not do any tkinter
stuff from threads. If you wanted to write the pastebinning program in tkinter,
you would need to do quite a few things yourself:
Create a
Queue
that will contain texts ofself.url_label
. Queues can be used from threads and from Tk’s event loop, so we can use them to communicate between the pastebinning thread and the event loop.Create a method that gets a text from the queue and sets it to
self.url_label
.- This method needs to use the tkinter label, so it must not be called from a thread because tkinter and threads don’t mix well. The only other way to call it is from tkinter’s event loop.
- Because the method is called from tkinter’s event loop, it must not block; that is, it can’t wait until a message arrives to the queue from the thread. If there are no messages in the queue, it must do nothing.
- Because the method can’t wait for messages in the thread, and it can only check if there are messages, it must be ran repeatedly e.g. 20 times per second. The thread will then add a message to the queue, and the queue clearing method will do something with that message soon.
- Because the method must be called 20 times per second, you must tell the event loop to run it 20 times per second. The only way to do this is with after callbacks.
If you accidentally call a tkinter thing from a thread, you may get very weird
behaviour (but in teek, you get a RuntimeError
as shown above):
- Things may work 90% of the time and break 10% of the time.
- Everything may work just fine on your computer but not on someone else’s computer.
- When things break, you get confusing error messages that don’t necessarily say anything about threads.
Furthermore, beginners often want to use threads with tkinter, and they struggle with it a lot, which is no surprise. Threading with tkinter is hard.
Teek’s init_threads()
does the hard things for you:
-
teek.
init_threads
(poll_interval_ms=50)[source]¶ Allow using teek from other threads than the main thread.
This is implemented with a queue. This function starts an after callback that checks for new messages in the queue every 50 milliseconds (that is, 20 times per second), and when another thread calls a teek function that does a Tcl call, the information required for making the Tcl call is put to the queue and the Tcl call is done by the after callback.
Note
After callbacks don’t work without the event loop, so make sure to run the event loop with
run()
after callinginit_threads()
.poll_interval_ms
can be given to specify a different interval than 50 milliseconds.When a Tcl call is done from another thread, that thread blocks until the after callback has handled it, which is slow. If this is a problem, there are two things you can do:
- Use a smaller
poll_interval_ms
. Watch your CPU usage though; if you makepoll_interval_ms
too small, you might get 100% CPU usage when your program is doing nothing. - Try to rewrite the program so that it does less teek stuff in threads.
- Use a smaller
If you use init_threads()
, you can also use this decorator:
-
teek.
make_thread_safe
(func)[source]¶ A decorator that makes a function safe to be called from any thread.
Functions decorated with this always run in the event loop, and therefore in the main thread.
Most of the time you don’t need to use this yourself; teek uses this a lot internally, so most teek things are already thread safe. However, if you have code like this…
def bad_func123(): func1() func2() func3()
…where
func1
,func2
andfunc3
do teek things and you need to callfunc123
from a thread, it’s best to decoratefunc123
:@teek.make_thread_safe def good_func123(): func1() func2() func3()
This may make
func123
noticably faster. If a function decorated withmake_thread_safe()
is called from some other thread than the main thread, it needs to communicate between the main thread and teek’s event loop, which is slow. However, withgood_func123
, there isn’t much communication to do: the other thread needs to tell the main thread to run the function, and later the main thread tells the other thread that the function has finished running. Thebad_func123
function does this 3 times, once in each line of code.Note
Functions decorated with
make_thread_safe()
must not block because they are ran in the event loop. In other words, this code is bad, because it will freeze the GUI for about 5 seconds:@teek.make_thread_safe def do_stuff(): time.sleep(5)
Letting the user know that something is happening¶
Here is a part of our example program above.
def __init__(self, *args, **kwargs):
...
self.paste_button = teek.Button(self, "Pastebin a hello world", self.start_pasting)
...
def start_pasting(self):
threading.Thread(target=self.paste).start()
def paste(self):
self.url_label.config['text'] = "Pasting..."
...
self.url_label.config['text'] = url
Can you see the problem? The paste button can be clicked while paste()
is
running in the thread. If the user does that, we have two pastes running at the
same time in different threads. That’s not nice.
A simple alternative is to make the button grayed out in the paste function:
def paste(self):
self.paste_button.state.add('disabled')
self.url_label.config['text'] = "Pasting..."
...
self.url_label.config['text'] = url
self.paste_button.state.remove('disabled')
See Widget.state
for documentation about the state thing.
If you don’t want to disable widgets or you would need to disable a widget and
all widgets in it, you can use Widget.busy()
instead, like this:
def paste(self):
with self.busy():
self.url_label.config['text'] = "Pasting..."
...
self.url_label.config['text'] = url
Here is the reference.
-
Widget.
busy_status
()[source]¶ See
tk busy status
in busy(3tk).This Returns True or False.
New in Tk 8.6.
-
Widget.
busy
()[source]¶ A context manager that calls
busy_hold()
andbusy_forget()
.Example:
with window.busy(): # window.busy_hold() has been called, do something ... # now window.busy_forget() has been called
For more advanced things, you can also use a separate Progressbar
widget.
After Callbacks¶
Sometimes threads are overkill. Here is a clock program:
import threading
import time
import teek
class Clock(teek.Frame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.label = teek.Label(self)
self.label.pack()
threading.Thread(target=self.updater_thread).start()
def updater_thread(self):
while True:
self.label.config['text'] = time.asctime()
time.sleep(1)
teek.init_threads()
window = teek.Window("Clock")
Clock(window).pack()
window.on_delete_window.connect(teek.quit)
teek.run()
BTW
If you close the window that the above program creates, you get a
RuntimeError
saying that init_threads()
wasn’t called,
because closing the window calls quit()
and init_threads()
would need to be called again after a quit()
. You can make the
program exit cleanly by replacing this…
self.label.config['text'] = time.asctime()
…with this:
try:
self.label.config['text'] = time.asctime()
except RuntimeError:
# the program is quitting
return
Returning from the thread target will stop the thread, and Python will exit because no more threads are running.
The repeatedly called time.sleep(1)
in updater_thread()
tells you that
after callbacks might be a better alternative. They work like this:
import time
import teek
class Clock(teek.Frame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.label = teek.Label(self)
self.label.pack()
self.updater_callback()
def updater_callback(self):
self.label.config['text'] = time.asctime()
# tell tk to run this again after 1 second
teek.after(1000, self.updater_callback)
window = teek.Window("Clock")
Clock(window).pack()
window.on_delete_window.connect(teek.quit)
teek.run()
teek.after(1000, self.updater_callback)
runs self.updater_callback()
in
Tk’s event loop after 1000 milliseconds; that is, 1 second.
-
teek.
after
(ms, callback, args=(), kwargs=None)[source]¶ Run
callback(*args, **kwargs)
after waiting for the given time.The ms argument should be a waiting time in milliseconds, and kwargs defaults to
{}
. This returns a timeout object with acancel()
method that takes no arguments; you can use that to cancel the timeout before it runs.
-
teek.
after_idle
(callback, args=(), kwargs=None)[source]¶ Like
after()
, but runs the timeout as soon as possible.
See also after(3tcl).
It’s also possible to cancel a timeout before it runs. after()
and
after_idle()
return timeout objects, which have a method for
canceling:
-
timeout_object.
cancel
()¶ Prevent this timeout from running as scheduled.
RuntimeError
is raised if the timeout has already ran or it has been cancelled.
Timeout objects also have a useful string representation for debugging:
>>> teek.after(1000, print) # doctest: +ELLIPSIS
<pending 'print' timeout 'after#...'>