Search code examples
pythonmultithreadingbeeware

Use of RLock in Beeware Toga app: GUI blocked when running a background thread


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?


Solution

  • 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