Multithreading and the Event Loop
Kivy runs a single UI thread - the event loop that processes touches, renders frames, and fires property events - and anything that blocks it freezes the app. The rule that governs all concurrency in Kivy: the UI thread renders and reacts; every long-running job runs elsewhere, and its result comes back through the Clock. This chapter is the general version of the pattern the networking chapter used for HTTP - the Clock and its scheduling, threading for the work, the threadsafe decorator, and the design rules that keep a multi-threaded Kivy app correct.
The Event Loop and the UI Thread
When App.run() starts, Kivy enters the main loop: it pumps OS events (touches, keys, window messages), processes the Clock callbacks that are due, fires property changes, and renders a frame - repeating at the display's frame rate. Every widget, every property binding, and every on_* handler runs on this thread, which is why a time.sleep(2) or a file read in a button handler freezes the window for two seconds. The Clock is the loop's scheduler: it holds callbacks with a time (or a frame count) at which to run, and it is the only sanctioned way to get code onto the UI thread from anywhere else. Understanding that the loop is single-threaded and that the Clock is its door is the entire mental model the rest of the chapter builds on.
The Clock: Scheduling on the UI Thread
from kivy.clock import Clock
# a one-shot: run once after 1.0 seconds
def greet(dt):
print("hello after a second")
Clock.schedule_once(greet, 1.0)
# a repeating: run every 0.5 seconds until cancelled
def tick(dt):
print("tick", dt) # dt is the elapsed time since last call
timer = Clock.schedule_interval(tick, 0.5)
# ... later, to stop it:
# Clock.unschedule(timer)
# frame-based: run on the Nth frame from now
# Clock.schedule_frame(callback) - the render-loop-native scheduling
Clock.schedule_once(func, seconds) runs func once after the delay; Clock.schedule_interval(func, seconds) runs it repeatedly and returns a handle you pass to Clock.unschedule to stop it. The callback receives dt (the time since the last call), which is what a game loop or an animation uses for frame-rate-independent motion. Clock.schedule_frame schedules on a frame count instead of a time - the low-level form for render-loop work. The Clock is also the debounce and throttle tool: scheduling a one-shot and cancelling the previous one before scheduling the next is the standard "run this at most once per interval" pattern, which the data-driven chapter used to keep a list refresh from running on every keystroke.
Threads: The Work, and the Clock Back
import threading
from kivy.clock import Clock
def heavy_compute(n):
# runs in the worker thread: CPU work, I/O, anything slow
total = sum(i * i for i in range(n))
return total
class ResultsScreen:
def __init__(self):
self.label = None
self._busy = False
def start(self, n):
if self._busy:
return # don't stack the work
self._busy = True
self.label.text = "Computing..."
threading.Thread(target=self._worker, args=(n,),
daemon=True).start()
def _worker(self, n):
result = heavy_compute(n) # off the UI thread
# bring it back - the ONLY way to touch widgets from here:
Clock.schedule_once(lambda dt: self._done(result), 0)
def _done(self, result):
self.label.text = f"Result: {result}"
self._busy = False
The general shape: the UI thread sets the busy state and starts a daemon Thread; the worker does the work and touches no widgets; the result returns via Clock.schedule_once(..., 0), which places the callback on the next event-loop iteration, where widget access is safe. The _busy guard prevents stacking work (a second tap does not start a second thread racing the first). This is the same three-step pattern as the networking chapter, generalized: any job - compute, file I/O, a model inference - goes out on a thread and comes back on the Clock. The dt of 0 means "the next loop iteration," the standard handoff delay.
The threadsafe Decorator
from kivy.clock import Clock
# kivy provides a helper that wraps the "schedule back" for you:
from kivy.support import ... # conceptually; the pattern is:
def threadsafe(func):
"""Wrap a method so it is scheduled onto the UI thread."""
def wrapper(self, *args, **kwargs):
Clock.schedule_once(lambda dt: func(self, *args, **kwargs), 0)
return wrapper
return wrapper
class Counter:
def __init__(self):
self.value = 0
self.label = None
@threadsafe
def bump(self):
# always runs on the UI thread, even if called from a worker
self.value += 1
self.label.text = str(self.value)
def worker(self):
import threading
for _ in range(1000):
self.bump() # called from the worker, safe
When several workers update the same UI, the per-call Clock.schedule_once is repetitive, and a threadsafe decorator removes the noise: any method marked with it is always scheduled onto the UI thread before running, so a worker can call self.bump() directly and the scheduling happens inside. The decorator is the ergonomic layer over the same handoff - the pattern is unchanged (the work happens where it happens, the UI mutation is scheduled), and the decorator just makes the call site clean. The caution that applies to the manual form applies here: the decorator schedules, it does not lock - if two workers mutate the same data (not just the UI), a threading.Lock on the data is still the correct tool; the Clock serializes the UI thread, not your data structures.
The Design Rules
- One UI thread, always. Widgets, properties, and the
Clockare not thread-safe; all of them run on the event-loop thread, and only theClockputs code there. - Work out, result back. The thread does the job; the
Clockdelivers the result. A worker that touches a widget directly is a data race, not a style choice. - Guard against stacking. A
_busyflag (or a single worker) prevents a burst of triggers from starting racing threads. - Lock the data, not just the UI. The
Clockserializes widget access; shared data structures mutated by workers need their ownthreading.Lock. - Daemon threads for app work. A daemon worker dies with the app, so a hung job cannot keep the process alive after the window closes.
- Throttle the UI-facing rate. A worker that produces 1000 updates a second should schedule them to the UI at the frame rate (a
Clockthrottle), not flood the loop.
Summary
Kivy is single-threaded on the UI: the event loop renders, reacts, and fires property events, and anything that blocks it freezes the app. The Clock is the loop's scheduler - schedule_once for a delayed one-shot, schedule_interval for a repeatable (with unschedule to stop), schedule_frame for frame-based work - and it is the only door onto the UI thread. The concurrency pattern is thread-out/clock-back: the UI sets a busy state and starts a daemon thread, the worker does the job and touches no widgets, and the result returns via Clock.schedule_once(..., 0); a threadsafe decorator makes the handoff a call-site detail. Lock the shared data separately from the UI serialization, guard against stacking work, and throttle the update rate to the frame rate - and a multi-threaded Kivy app stays responsive and correct, because the one rule (the UI thread renders, the Clock is the door) holds everywhere.