I'm trying to develop a simple application where there is a background task that I want to turn on and off with a switch button. That background task will show once per second a random number on a GUI label.
My current code is:
import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW
import time, threading, random
class MainApp(toga.App):
def startup(self):
main_box = toga.Box(style=Pack(direction=COLUMN))
self.value = toga.Label("", style=Pack(flex=1))
button_1 = toga.Button(
"start/stop", on_press=self.background, style=Pack(padding=5)
)
main_box.add(self.value)
main_box.add(button_1)
self.main_window = toga.MainWindow(title=self.formal_name)
self.main_window.content = main_box
self.main_window.show()
self.ev = threading.Event()
self.lock = threading.RLock()
def background(self, widget):
if self.ev.is_set():
self.ev.clear()
else:
self.ev.set()
th = threading.Thread(target=self.print_numbers)
th.start()
def print_numbers(self):
# time.sleep(0.1)
while self.ev.is_set():
value = random.randint(0, 100)
# with self.lock:
self.value.text = value
time.sleep(1)
def main():
return MainApp("Application", "com.example")
However when I start the application and press the button to start the background task, the application freeze.
As reported in this question, it is caused by a time competition between the two threads (background and main), and setting time.sleep(0.1)
, as the user report, solve the problem (temporarely).
As suggested in the comments of the same question, I tried to implement the thread synchronization APIs RLock
, but when I run the application with the lock (the line commented in my code) it doesn't show anything
Am I missing something? Is that the right implementation of RLock
? The is some workaround for this issue?
it is caused by a time competition between the two threads (background and main)
I don't think so, because the background thread is hardly doing any work. It's more likely to be caused by accessing the GUI on a background thread, which is not reliable on Toga or most other GUI toolkits.
In this simple example, you can turn your background task into an async function and avoid multi-threading completely:
import asyncio
...
self.ev = asyncio.Event()
def background(self, widget):
if self.ev.is_set():
self.ev.clear()
else:
self.ev.set()
# self.loop requires Toga 0.4 or later.
self.loop.create_task(self.print_numbers())
async def print_numbers(self):
while self.ev.is_set():
value = random.randint(0, 100)
self.value.text = value
await asyncio.sleep(1)
Or if you need the background task to do some CPU-intensive work, this can easily be moved to a separate thread using run_in_executor
:
async def print_numbers(self):
while self.ev.is_set():
value = await self.loop.run_in_executor(
None, some_slow_function, arg1, arg2, ...
)
self.value.text = value