Search code examples
pythonmultithreadinguser-interfacegtkthread-synchronization

Thread synchronization in GTK3+ and Python


I'm currently learning both Python and GTK 3+ and I've a problem when synchronizing threads. I'll try to be quick and clear:

I have to make a Social Network client. As the purpose is to learn how to create a GUI the "access to the social network API" will be simulated, but I have to "wait" for network responses with time.sleep(). Calling time.sleep() in the main thread freezes the GUI (it stops the execution of Gtk.Main()) so I have to make all my connections in a separate thread. And here lies my problem. When I'm authenticating a user (verifying_credentials) I need to wait from that thread to finish to continue the execution of the main program. If I try a Thread.join GUI freezes. I've tried using queues, but queue.get is also blocking Gtk.main(). I've tried emitting a signal when my thread is finished , but the handler starts in the same thread, so when I try to modify the GUI (which I need) , program crashes (you're not supposed to touch the GUI from anywhere except main thread).

My solution ? I do busy-waiting / active-waiting , which is by definition an antipattern. I keep asking if the thread has finished and forcing cicles of Gtk.main()

There has to be another way, a more elegant / efficient way than mine. I don't know if I can signal another thread, or there is a way of using queues without blocking the main thread. Any help will be very much appreciated.

The python code :

from os.path import abspath, dirname, join
import gettext
import threading
import math
import time
import random
import locale

from gi.repository import Gtk, Gdk, GLib, GObject
import list

APP = "UDC_Social_API"
user_list = {
    "user": "password",
    "admin": "admin",
    "carnotan": "1234"
}

class UDC_Social_API:

    def on_applicationwindow1_key_press_event(self, widget, event):
        # keyname = Gdk.keyval_name(event.keyval)
        # print (_("Key %(name)s (%(val)d) was pressed" )%{"name":keyname, "val":event.keyval})
        if event.keyval == 65293:
            self.on_login_button_clicked()

    def delay(self):
        if self.delay_option.get_active():
            time.sleep(math.exp(random.random()*5))
        else:
            pass

    def active_waiting(self):
        while self.finished is False:
            Gtk.main_iteration_do(False)
        self.finished = False
        self.z_handler(None)

    def verify_credentials(self, user, password):
        GLib.idle_add(self.active_waiting)
        self.delay()
        if user in user_list:
            if password == user_list.get(user):
                self.authentication = True
                self.finished = True
            else:
                self.authentication = False
                self.finished = True
        else:
            self.authentication = False
            self.finished = True

    def on_login_button_clicked(self, data=None):
        user = self.user_entry.get_text()
        password = self.password_entry.get_text()
        thread = threading.Thread(target=self.verify_credentials, args=(user, password))
        thread.daemon = True
        thread.start()

    def z_handler(self, data=None):
        if self.authentication is False:
            self.message_dialog.set_markup(_("User/Password incorrect\nPlease, verify login information"))
            self.message_dialog.run()
            self.message_dialog.hide()
            return False
        else:
            self.window.hide()
            print ("Success!")



    def on_applicationwindow1_destroy(self, data=None):
        Gtk.main_quit()

    def on_gtk_about_activate(self, menuitem, data=None):
        self.aboutdialog.run()
        self.aboutdialog.hide()

    def on_gtk_cut_activate(self, widget):
        # Get the bounds of the selected text
        bounds = self.focus.get_selection_bounds()
        # if the bounds of the selection are not an empty tuple,
        # put the selection in the variable chars
        # and copy it to the clipboard
        # (get_selection_bounds returns an empty tuple if there is no selection)
        # then delete the selection
        if bounds:
            chars = self.focus.get_chars(*bounds)
            self.clipboard.set_text(chars, -1)
            self.focus.delete_text(bounds[0], bounds[1])
        else:
            pass

    def on_gtk_copy_activate(self, widget):
        # Get the bounds of the selected text
        bounds = self.focus.get_selection_bounds()
        # if the bounds of the selection are not an empty tuple,
        # put the selection in the variable chars
        # and copy it to the clipboard
        # (get_selection_bounds returns an empty tuple if there is no selection)
        if bounds:
            chars = self.focus.get_chars(*bounds)
            self.clipboard.set_text(chars, -1)
        else:
            pass

    def on_gtk_paste_activate(self, widget):
        # Get the text from the clipboard
        text = self.clipboard.wait_for_text()
        if text is not None:
            # If there's text selected in the target
            # delete it and paste the contents of the clipboard
            bounds = self.focus.get_selection_bounds()
            if bounds:
                self.focus.delete_text(bounds[0], bounds[1])
                self.focus.insert_text(text, bounds[0])

            # else insert the text in the current position of the cursor in the target
            else:
                pos = self.focus.get_position()
                self.focus.insert_text(text, pos)
        else:
            pass

    def on_entry_focus(self, widget, event):
        self.focus = widget

    def create_menubar(self):
        self.file_menu=self.builder.get_object("menuitem1")
        self.edit_menu=self.builder.get_object("menuitem2")
        self.options_menu=self.builder.get_object("option")
        self.help_menu=self.builder.get_object("menuitem4")
        self.languages_menu=self.builder.get_object("menuitem3")
        self.delay_option = self.builder.get_object("delay_option")
        self.gtk_quit_menu=self.builder.get_object("gtk_quit_menu")
        self.gtk_cut_menu=self.builder.get_object("gtk_cut_menu")
        self.gtk_copy_menu=self.builder.get_object("gtk_copy_menu")
        self.gtk_paste_menu=self.builder.get_object("gtk_paste_menu")
        self.gtk_about_menu=self.builder.get_object("gtk_about_menu")
        self.galician_option=self.builder.get_object("radiomenuitem1")
        self.spanish_option=self.builder.get_object("radiomenuitem2")
        self.english_option=self.builder.get_object("radiomenuitem3")

    def set_menubar_names(self):
        self.file_menu.set_label(_("_File"))
        self.edit_menu.set_label(_("_Edit"))
        self.options_menu.set_label(_("_Options"))
        self.help_menu.set_label(_("_Help"))
        self.languages_menu.set_label(_("_Languages"))
        self.delay_option.set_label(_("_Delay"))
        self.gtk_quit_menu.set_label(_("Quit"))
        self.gtk_copy_menu.set_label(_("Copy"))
        self.gtk_cut_menu.set_label(_("Cut"))
        self.gtk_paste_menu.set_label(_("Paste"))
        self.gtk_about_menu.set_label(_("About"))
        self.galician_option.set_label(_("_Galician"))
        self.spanish_option.set_label(_("_Spanish"))
        self.english_option.set_label(_("_English"))

    def create_login_box(self):
        self.user_entry = self.builder.get_object("user_entry")
        self.password_entry = self.builder.get_object("password_entry")
        self.user_label=self.builder.get_object("user_label")
        self.password_label=self.builder.get_object("password_label")
        self.login_button=self.builder.get_object("login_button")

    def set_login_box_names(self):
        self.user_entry.set_placeholder_text(_("user"))
        self.password_entry.set_placeholder_text(_("password"))
        self.user_label.set_label(_("User"))
        self.password_label.set_label(_("Password"))
        self.login_button.set_label(_("Login"))

    def create_about_dialog(self):
        self.aboutdialog = self.builder.get_object("aboutdialog1")
        self.aboutdialog.set_transient_for(self.window)
    def set_about_dialog(self):
        self.aboutdialog.set_comments(_("Developed for GTK 3+ and Python 3.4"))

    def reset_names(self):
        self.set_menubar_names()
        self.set_login_box_names()

    def on_radiomenuitem1_toggled(self, widget):
        if widget.get_active():
            self.lang_gl_ES.install()
            self.reset_names()
            self.window.queue_draw()
        else:
            pass

    def on_radiomenuitem2_toggled(self, widget):
        if widget.get_active():
            self.lang_es_ES.install()
            self.reset_names()
            self.window.queue_draw()
        else:
            pass

    def on_radiomenuitem3_toggled(self,widget):
        if widget.get_active():
            self.lang_en_US.install()
            self.set_menubar_names()
            self.window.queue_draw()
        else:
            pass

    def set_languages(self):
        WHERE_AM_I = abspath(dirname(__file__))
        locale.setlocale(locale.LC_ALL, '')
        locale.bindtextdomain(APP, WHERE_AM_I)
        locale_path = WHERE_AM_I +'/'
        self.builder.set_translation_domain(APP)
        gettext.find(APP,localedir=locale_path,languages=['gl_ES'])
        gettext.find(APP,localedir=locale_path,languages=['es_ES'])
        gettext.find(APP,localedir=locale_path,languages=['en_US'])
        gettext.install(APP,locale_path)
        gettext.textdomain(APP)
        gettext.bindtextdomain(APP,locale_path)
        self.lang_gl_ES=gettext.translation(APP,localedir=locale_path, languages=['gl_ES'])
        self.lang_es_ES=gettext.translation(APP,localedir=locale_path, languages=['es_ES'])
        self.lang_en_US=gettext.translation(APP,localedir=locale_path, languages=['en_US'])

    def set_signals(self):
        handlers = {
            "on_applicationwindow1_destroy": self.on_applicationwindow1_destroy,
            "on_gtk_about_activate": self.on_gtk_about_activate,
            "on_login_button_clicked": self.on_login_button_clicked,
            "on_applicationwindow1_key_press_event": self.on_applicationwindow1_key_press_event,
            "on_entry_focus": self.on_entry_focus,
            "on_gtk_cut_activate": self.on_gtk_cut_activate,
            "on_gtk_copy_activate": self.on_gtk_copy_activate,
            "on_gtk_paste_activate": self.on_gtk_paste_activate,
            "on_radiomenuitem1_toggled": self.on_radiomenuitem1_toggled,
            "on_radiomenuitem2_toggled": self.on_radiomenuitem2_toggled,
            "on_radiomenuitem3_toggled": self.on_radiomenuitem3_toggled
        }
        self.builder.connect_signals(handlers)

    def __init__(self):
        # GObject.signal_new("z_signal", Gtk.ApplicationWindow, GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ())
        self.builder = Gtk.Builder()
        self.builder.add_from_file("p1.glade")
        self.window = self.builder.get_object("applicationwindow1")
        self.set_languages()
        self.create_menubar()
        self.create_login_box()
        self.create_about_dialog()
        self.reset_names()
        self.set_signals()
        self.focus = None
        self.finished = False
        self.authentication = False
        # self.statusbar = self.builder.get_object("statusbar1")
        # self.context_id = self.statusbar.get_context_id("status")
        # self.status_count = 0
        self.message_dialog = self.builder.get_object("messagedialog1")
        self.message_dialog.set_transient_for(self.window)
        self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        self.window.show_all()

if __name__ == "__main__":
    GObject.threads_init()
    main = UDC_Social_API()
    Gtk.main()

Glade file is in pastebin, because it will exceed post size limit.

http://pastebin.com/8S3k7f6J

Thanks in advance for any help you could provide.


Solution

  • You can use GLib.idle_add to schedule a callback to be executed by the event loop in the main thread of your program. This means it provides a safe way to schedule a GUI update from a background thread. So, you can just let your background thread run normally, let the main thread return control to the event loop, and then make the appropriate GUI updates from the background thread via GLib.idle_add once it's done:

    def verify_credentials(self, user, password):
        self.delay()
        if user in user_list:
            if password == user_list.get(user):
                self.authentication = True
            else:
                self.authentication = False
        else:
            self.authentication = False
        # Schedule z_handler to be called by the event loop in the main thread.
        GLib.idle_add(z_handler, None) 
    
    def z_handler(self, data=None):
        if not self.authentication:
            self.message_dialog.set_markup(_("User/Password incorrect\nPlease, verify login information"))
            self.message_dialog.run()
            self.message_dialog.hide()
            return False
        else:
            self.window.hide()
            print ("Success!")
    

    You're actually pretty close to using this same method, you're just doing it in an awkward way - you're scheduling active_waiting to run in the main thread, which waits until the background thread is done, and then calls z_handler. Scheduling z_handler directly, after the background thread is done with its work, is much simpler.