Threading 1.3: Success Story

Dear @jeff.bezanson and @jameson. Just want to share a success story for the new threading infrastructure (1.3 alpha). The focus in your Juliacon talk seemed to be primary on performance but from my perspective a very important advantage is asynchronous UI programming. In UI programming it is very common to move all work to threads such that the UI remains responsive. This was not really doable until Julia 1.3. The following Gtk example shows what is now possible. One clicks a button and a progress bar is updated regularly, depending on the work done. When running the code with one thread, the entire application freezes. When using more than one thread, everything works as expected.

What is not 100% clear to me is if it is actually allowed to update the UI on the thread. If not one would need to handle over the work to the UI thread somehow. Maybe spawn a task that is scheduled on the main thread.

using Gtk.ShortNames

b = Box(:v)
win = Window(b,"test")

button = Button("do work")
push!(b,button)

pb = ProgressBar()
push!(b,pb)

Gtk.showall(win)

function doWork()
  N = 20
  for k=1:N
    set_gtk_property!(pb,:fraction,(k-1)/(N-1) )
    sum(collect(1:100000000)) # some heavy work
  end
end

signal_connect(button, "clicked") do widget
  Threads.@spawn doWork()
end
14 Likes

To answer the question myself, the following runs the UI updating code on the main thread again:

using Gtk.ShortNames

b = Box(:v)
win = Window(b,"test")

button = Button("do work")
push!(b,button)

pb = ProgressBar()
push!(b,pb)

Gtk.showall(win)


function doWork()
  N = 20
  for k=1:N
    Gtk.GLib.g_idle_add(nothing) do user_data
      set_gtk_property!(pb,:fraction,(k-1)/(N-1) )
      Cint(false)
    end
    sum(collect(1:100000000)) # some heavy work
  end
end

signal_connect(button, "clicked") do widget
  Threads.@spawn doWork()
end
1 Like

According to the GDK manual, GTK+ is not thread-safe and you should call gdk_threads_add_idle with a callback function in order to queue GTK+ calls from another thread.

Passing C callback functions from Julia is pretty straightforward if you know what you are doing, but it might be nice if Gtk.jl made it a bit easier by adding a wrapper, as I commented at https://github.com/JuliaGraphics/Gtk.jl/pull/395

2 Likes

yes, then my second solution seems to be appropriate. Thanks.

Maybe some of the performance issues with the REPL (specially on Windows) could be solved, but we would need someone that understand well these problems to look at Gtk.jl core code.

Maybe some of the performance issues with the REPL (specially on Windows ) could be solved, but we would need someone that understand well these problems to look at Gtk.jl core code

Maybe that person could be the same as the one responsible for the new threading work… :slight_smile:

It’s great that this works, but technically @spawn is not guaranteed to run on a separate thread — it’s for expressing that parallelism is available, not mandatory. I think we’ll need a different word/mechanism here. One possibility is for us to always run the event loop on a dedicated thread.

maybe the word spawn is a little bit misleading then? I would have expected that it runs the function to be threaded on a “free” thread which is not the main thread. I.e. that the thread pool is more or less an implementation detail.

Yes, exactly — that would be another alternative, similar to having a dedicated event loop thread. But spawn still could not guarantee which threads tasks run on. For example given

x = 0
@spawn (while x == 0; end; fire_missiles())
@spawn x = 1

it’s not defined whether the missiles will be fired. Cilk has the same design consideration. And of course that example can be generalized to any number of threads. The fixed-size thread pool can’t quite be an implementation detail, for this reason. That’s why some kind of different mechanism is needed here.

1 Like

:frowning: Can’t we at least prevent the spawned code to be run on the main thread? I hoped so much that spawn will solve this problem…

As I said, yes we could potentially add something like @spawn background .... That handles the case where a single dedicated event/UI thread is sufficient. But what does it do when nthreads==1? It could mean that such a mode no longer exists, which might indeed be the future, but requires some thought.

Instead of g_idle_add, I think for better scalability, you’d want to do separate your value computation from the rendering (important caveat: it’s been years since I’ve actually done any “serious” GUI programming):

# application state struct
const pct_complete = Ref(0.0)
const dirty = Threads.Event()

# on work thread
while <>
    global pct_complete[] = <fraction>
    <significant_change> && notify(dirty)
end

# on main (gui) thread
while !exit
    wait(dirty)
    dirty.set = false # todo: wrap this in a function?
    # now update everything that has changed (c.f. React.js)
    p = pct_complete[]
    pb.fraction[] = p
    # sleep(0.016) -- optionally limit the update interval to 60 fps to avoid spamming Gtk with updates
end

Note that this assumes that updates to the state can appear asynchronously (e.g. potentially partial data updates and out-of-order) with a simple pointer update (or in this case, a Float64) assignment. If you have more complex data, you might require a lock around the data update, just so you’re aware.

Dynamically increase the number of threads :wink:

Ok — if we’re at the point where we can add new threads at run time and we want @spawn background ... then yes, I guess that could add a background thread if one doesn’t exist yet.

1 Like

yes, that is an alternative, but then I would run two threads, the worker thread and some progressBar updating thread. Main thread should idle so that the user can make UI changes (i.e. cancel the operation)

The input (user control) and output (render updates) threads are required to be the same. I agree with the principle though, that you would want to ensure the input and output operations done on that thread are lightweight, and any “heavy lifting” is done in the worker thread(s).

I have yet to page in any changes in how this is all implemented, but IIRC, @spawn does not yield on task creation in some situations. If there are available threads, and the spawning thread has not yielded on task creation (or shortly thereafter), then the new task will be picked up by one of the free threads. Then, so long as this new task doesn’t yield, it will continue to be run by that thread. This is functionality that is broadly useful, so if this isn’t available currently, I’ll be sure to open a PR to add it soonish.

Tobias: thanks for the post, very happy to see this in finally, and being used effectively!

3 Likes