3

Question: What is the right way to not block a GTK UI while loading data from a remote HTTP source?

Background:

I'm working on a cryptocurrency price indicator for Ubuntu Unity. I've modified the original project so that it can run multiple tickers at the same time.

Schematically, it works like this:

The main thread finds every ticker to be loaded and spins up a GTK libAppindicator instance (Indicator) for each. Each indicator then loads a class that retrieves ticker data from the remote API and feeds it back to the indicator.

However, I noticed that the UI becomes irresponsive at irregular intervals. I assume that this is because the HTTP requests (with the requests library) are blocking. So I decided to start every Indicator in its own thread, but the same issue still occurs once the tickers are all loaded. (I don't understand why, if something is on its own thread, it should not block the main thread?)

I've then tried to replace requests with grequests, however I can't get this to work as the callback seems to never get called. There also seems to be something like promises, but the documentation for all of this looks outdated and incomplete.

Code:

# necessary imports here

class Coin(Object):
    self.start_main()
    self.instances = []

    def start_main(self):
        self.main_item = AppIndicator.Indicator.new(name, icon)
        self.main_item.set_menu(self._menu()) # loads menu and related actions (code omitted)

    def add_indicator(self, settings=None):
        indicator = Indicator(self, len(self.instances), self.config, settings)
        self.instances.append(indicator)
        nt = threading.Thread(target=indicator.start())
        nt.start()

    def _add_ticker(self, widget): # this is clickable from the menu (code omitted)
        self.add_indicator('DEFAULTS')

class Indicator():
    instances = []

    def __init__(self, coin, counter, config, settings=None):
        Indicator.instances.append(self)

    def start(self):
        self.indicator = AppIndicator.Indicator.new(name + "_" + str(len(self.instances)), icon)
        self.indicator.set_menu(self._menu())
        self._start_exchange()

    def set_data(self, label, bid, high, low, ask, volume=None):
        # sets data in GUI

    def _start_exchange(self):
        exchange = Kraken(self.config, self)
        exchange.check_price()
        exchange.start()

class Kraken:
  def __init__(self, config, indicator):
    self.indicator = indicator
    self.timeout_id = 0

  def start(self):
    self.timeout_id = GLib.timeout_add_seconds(refresh, self.check_price)

  def stop(self):
    if self.timeout_id:
      GLib.source_remove(self.timeout_id)

  def check_price(self):
    res = requests.get('https://api.kraken.com/0/public/Ticker?pair=XXBTZUSD')
    data = res.json()
    self._parse_result(data['result'])

  def _parse_result(self, data):
    # code to parse json result
    self.indicator.set_data(label, bid, high, low, ask)

coin = Coin()
Gtk.main()
bluppfisk
  • 2,538
  • 3
  • 27
  • 56
  • run HTTP in `thread` – furas Dec 12 '17 at 07:52
  • better show you code. Maybe in new thread you use GUI element which blocks `mainloop` in GUI, or you send data to main thread and it block `mainloop` when GUI receives data. – furas Dec 12 '17 at 07:54
  • I check most of your files and I found `import threading` only in `coin/indicator.py` but you never use it to create new threads. So where do you create those threads ? – furas Dec 12 '17 at 08:01
  • I'm not sure you you should run Indicator in thread because it is part ogf GUI. Thread should only download data. – furas Dec 12 '17 at 08:03
  • I haven't looked at your code. It's ok to link to your own code on external sites, but a Stack Overflow question has to be self-contained, so you should also post a [mcve] into the question itself that illustrates your problem. – PM 2Ring Dec 12 '17 at 08:04
  • I don't know GTK `libAppindicator` (and I haven't used GTK much in recent years), but I'd be inclined to run the GUI in the main thread, and launch separate worker threads to do the HTTP stuff. When a worker has data it sends an Event to its associated indicator widget. – PM 2Ring Dec 12 '17 at 08:05
  • @furas In start_main in coin.py, I'm spinning up an indicator instance in a thread. I realise it may be clumsy, because I'm certainly new to multithreading and even python. – bluppfisk Dec 12 '17 at 08:06
  • I don't know why you run `Gtk.main()` in new thread - it has to run in existing main thread with rest of GUI. – furas Dec 12 '17 at 08:13
  • @furas I used to do that, but then none of my other code would run until the Gtk loop finished running. – bluppfisk Dec 12 '17 at 08:24
  • @PM2Ring Fair point. I've extracted minimum viable code from the source and added it to the question. – bluppfisk Dec 12 '17 at 08:35
  • @furas, I mean after I call Gtk.main(), everything stops working. Cannot add new indicators etc. – bluppfisk Dec 12 '17 at 08:50
  • 1
    `Gtk.main()` runs loops which gets events from system, sends events to widgets, redraw widgets. If it is blocked then it can't refresh widgets and it looks like it freezes. It works all the time till you close window. You have to run other thread before `Gtk.main()` or after starting GUI using button in GUI. Or you can use `thread.Timer()` to create it before `Gtk.main()` but it will start after `Gtk.main()` starts. – furas Dec 12 '17 at 09:00
  • @furas, thanks, I've done that now and this part works. However, the GUI still locks up from time to time. For example, the about box will not show or close until an indicator is finished updating its data. It's not really noticeable when the connection is good, but e.g. with Kraken, it can block the UI for several seconds at a time. So it would seem that the threads are still able to block the main thread. – bluppfisk Dec 12 '17 at 09:16
  • Try [GLib GIO](https://lazka.github.io/pgi-docs/#Gio-2.0). – José Fonte Dec 13 '17 at 10:56

0 Answers0