Ensuring Gtk.jl responsivity

I’m writing a Gtk.jl application, and it’s easy to see that any time some time-consuming activity happens, such as running a long computation, this interferes with the UI responsivity. In especial, even updating a label with a message such as “Please wait while I perform your long computation” may not work, what can be quite annoying.

I imagine this must be due to something like the computations running in the main thread and blocking Gtk(.jl) from doing whatever it needs. What is a good way to move these computations away to a different thread? Or would tasks help? And maybe a more philosophical question: how to structure this program with computations running in parallel so that you have the same imperative step-by-step execution from the original blocking application?

Example app. You probably never get to see the first message in the label, only the final one with the computation result.

using Gtk
using LinearAlgebra


b = Gtk.Button("oi")
ll = Gtk.Label("output")
hb = Gtk.Box(:v)
w = Gtk.Window("x")

push!(hb,b)
push!(hb,ll)
push!(w, hb)

function calcpi(niter)
    acc = 0
    for n in 1:niter
        if norm(rand(2)) < 1.0
            acc += 1
        end
    end
    return 4*acc/niter
end

signal_connect(b, "clicked") do widget
    @info "Button clicked 1"
    GAccessor.text(ll,"Running your long calculation...")

    # niter = 100000000
    niter = 10000000
    # niter = 1000000
    mypi = calcpi(niter)
    @info mypi
    GAccessor.text(ll,"done, mypy=$mypi")
end


showall(w)

Some extra info: I have tried to put the calculation in a separate thread using @spawn, and even wrote a little polling loop that prints to the console while waiting for the calculation to stop. Still the UI freezes, not only the label does not get updated, the whole UI really freezes. Should I really actually finish the callback before the UI can be updated again?

Hey @xor0110,
this has been a long term issue, but with Julia 1.3 and its threading capabilities it actually works. Have a look at this example.

The point is to spawn the thread and put all UI code into an idle_add.

One missing piece in that code is to spawn the thread at a different thread than the master thread. There is some package exactly for that purpose, but I have not tried it yet (you need to search in the forum, I do not remember the name).

1 Like

Thanks, it’s good to hear from more people writing this kind of application. I think there’s a huge potential to be explored in Julia writing guis with libraries such a Gtk, which seems to be the most usable right now.

I got my application to do what I wanted, although I’m not entirely sure how it relates to you answer. What I noticed is you just have to make sure all your UI updates happen at the end of a callback call (or worker thread), and then as soon as that function is over the UI will be able to update. My scenario is different since I’m not really interested in interactive updates together with the calculations, which is of course a more interesting case.

Here’s my current example app. It requires @spawn, although I’m not even sure having actual separate threads is really required. A task taking over would work in this case since my problem is just that updates from before the calculation don’t even happen. It doesn’t really matter for me if it all gets blocked.

using Gtk
using LinearAlgebra
import Base.Threads.@spawn


b = Gtk.Button("oi")
ll = Gtk.Label("output")
hb = Gtk.Box(:v)
w = Gtk.Window("x")

push!(hb,b)
push!(hb,ll)
push!(w, hb)
state = :startcomp


signal_connect(b, "clicked") do widget
    global state
    if state == :startcomp
        @info "Button clicked 1"
        GAccessor.text(ll,"Running your long calculation...")
        state = :computing
        @spawn dolongcomp()
    end
end


function dolongcomp()
    global state

    niter = 100000000
    mypi = calcpi(niter)

    @info mypi
    GAccessor.text(ll,"done, mypy=$mypi")
    state = :startcomp
end


function calcpi(niter)
    acc = 0
    for n in 1:niter
        if norm(rand(2)) < 1.0
            acc += 1
        end
    end
    return 4*acc/niter
end


showall(w)

I had some great experience some time ago writing a Scala + Akka GUI where one actor runs in the main thread, and is the only one allowed to perform UI actions. Computations are performed by other actors which run in a separate thread pool, and communicate with the GUI actor. I would love to figure out how to do something like that in Julia. Tasks and Channels might provide enough capability for it, although I’m not sure how to even start. My example app right now is not so great because we have some logic tied straight to a button callback, and the other UI update should be triggered by some kind of custom signal. I don’t know if Gtk allows you to implement this.

I have implemented something similar. It uses a global struct where the computation thread updates the state and a UI thread/task updates the UI. I have been using a Timer for the UI task but you can also use @spawn with a thread and a sleep. But thinking longer about it, it would even better to use g_timeout_add :

This is a function that regularly calls a callback until the callback returns false.

For the global variable you should use const Ref to get no performance penalty.

1 Like

I tried replacing my @spawn call with g_idle_add and again I got the behavior where the UI doesn’t get updated before the computation starts.

That is clear. My suggestion was if you want to split the computation thread from the UI thread. g_idle_add should only get UI update to do. No computation in that callback.

OK, I think I get it then: to make sure something that needs to be run in the main thread goes there, we can use g_idle_add from whatever threads, is that it? One last question I would have, though is what actually really requires this. I’m not sure this is really the case from the GAccessor.text in my example, for instance.

yes, its not safe to call drawing code from arbitrary code and idle_add is the way to make an asynchron drawing operation. timeout_add is similar but allows repeated redrawing, which is useful, if you have a computation going on and want to monitor the computation with you UI.

I actually do not know, what operations are allowed and which not. You can google a little bit around what information you find on that. My understanding is that one should not change UI from a non-UI thread. And actually it makes, from my perspective, the code also cleaner if one uses an idle_add to trigger an asynchron UI update.

1 Like