-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Using multi threaded GTK app with Nim and Gintro #81
Comments
Does gintro 0.7.8 released yesterday work fine for you? Well I got no new issues yet, so maybe there are not too obvious issues? Multi threaded GTK is difficult indeed. Multi threaded Nim too. I am interested myself in this topic, for my chess game I would like to have a non blocking GUI. And for cairo drawing multiple threads would be nice too. But unfortunately cairo is from design single threaded. I have not really investigated this topic myself. Well I did some google search of course, there are simple solutions like the gtk idle function. The Nim cairo_anim is a very basic non blocking solution. Investigating this topic would be fun, but my time is too restricted for the next years, the GTK4 and the Nim book have priority, and there are many other tasks for me... I would propose that you ask on the Nim forum. Or maybe you ask on the GTK forum for C solutions, we should be able to convert it to Nim then. If you ask on Nim forum, you may ask for the other Nim GUI toolkits for threading as well, maybe some of the other GUIs has a good solution already. I think Nim has more than a dozen GUI toolkits now, I have listed some of them in the GTK4 book already. |
I used gintro 0.7.7. I've upgraded to 0.7.8 and everything is good. It is just this threading issue and it is not very nice.
|
https://developer.gnome.org/gtkmm-tutorial/unstable/sec-multithread-example.html.en |
So there is 1 way to do threading but for calling system command only: |
My test code. It worked perfectly:
|
here is an example to use multi threads in gtk with glib.g_idle_add. I hope you can help me try it on gintro @StefanSalewski |
It ran after i added
|
Great that you continued your investigations. I did a short google search myself about that topic, but was not too satisfied with the results yet. I consider the use of the GTK idle functions a bit ugly, it seems to be some sort of polling for results, but maybe that is the only solution that works with GTK. Have still to consider the message passing between thread in Nim, using Channels? I heard ARC would support better ways for thread communication, but I have still to investigate that. But maybe it is enough when we get an ugly but working solution for the start, we can improve it later. |
Yes i agree and it didn't work for me.
I tried it before with help from members in Telegram channel. It works. Well the problem is multiple channel and multiple threads. I tried that for creating GUI for ClamAV. The requirement was more complex than the basic syntax of GTK can do. So my idea is, if we can, we create a pre-built nim lang modules for threading in gtk with some basic check. But idk if it is good and further problems we can have. |
Have not yet read all your post carefully sorry, here is a fast hack using GTK3 timeoutAdd() and Nim channels. Nim thread does a countdown, we display that number as label on a button, we can click the button to invert the numbers. Well maybe not really nice, but it is a start, and was really easy. Works with arc, have not jet tested with GTK4. # nim c --threads:on --gc:arc -r t.nim
# https://nim-lang.org/docs/channels.html
import gintro/[gtk, glib, gobject, gio]
from os import sleep
var chan: Channel[int]
var worker: system.Thread[void]
proc work() =
var countdown {.global.} = 25
while countdown > 0:
sleep(1000)
dec(countdown)
chan.send(countdown)
proc invalidateCb(b: Button): bool =
let tried = chan.tryRecv()
if tried.dataAvailable:
#echo tried.msg
b.label = $tried.msg
if tried.msg == 0:
worker.joinThread
chan.close
return SOURCE_REMOVE
return SOURCE_CONTINUE
proc buttonClicked (button: Button) =
button.label = utf8Strreverse(button.label, -1)
proc appActivate (app: Application) =
let window = newApplicationWindow(app)
window.title = "Countdown"
window.defaultSize = (250, 50)
let button = newButton("Click Me")
window.add(button)
button.connect("clicked", buttonClicked)
window.showAll
chan.open
createThread(worker, work)
discard timeoutAdd(1000 div 60, invalidateCb, button)
proc main =
let app = newApplication("org.gtk.example")
connect(app, "activate", appActivate)
discard app.run
main() |
The gtkmm example from https://developer.gnome.org/gtkmm-tutorial/unstable/sec-multithread-example.html.en is indeed interesting, it seems to not need something like timeout_add(). But I think you can do all what you need following my example. The same principle should work with multiple threads and multiple channels. The idea is to pass data between treads over Nim channels and to use the iddleAdd() function to receive the data and to display them. You should be able to add buttons for starting or terminating threads. Maybe the most difficult part is to terminate a background process before it is finished, i.e. to terminate a chess engine when time limit is reached. We may need something like semaphores and global variables for that, you may ask in IRC or Nim forum. |
And here is a solution with g_idle_add(): # nim c --threads:on --gc:arc -r t.nim
import gintro/[gtk, glib, gobject, gio]
from os import sleep
var worker: system.Thread[void]
var button: Button
proc idleFunc(i: int): bool =
button.label = $i
return SOURCE_REMOVE
proc work() =
var countdown {.global.} = 25
while countdown > 0:
sleep(1000)
dec(countdown)
idleAdd(idleFunc, countdown)
proc buttonClicked (button: Button) =
button.label = utf8Strreverse(button.label, -1)
proc appActivate (app: Application) =
let window = newApplicationWindow(app)
window.title = "Countdown"
window.defaultSize = (250, 50)
button = newButton("Click Me")
window.add(button)
button.connect("clicked", buttonClicked)
window.showAll
createThread(worker, work)
proc main =
let app = newApplication("org.gtk.example")
connect(app, "activate", appActivate)
discard app.run
main() E. Bassi recommends use of GTask but we have to investigate how it can be used with Nim, see |
Thank you. I'm trying to understand it then trigger the worker (or idleAdd) after button is clicked. |
Well i still don't know how to use it on my code right now but as far as i'm understand:
|
Yes, threading is never easy. I have still no idea how to do it for my chess game, I would use some bidirectional message passing and a way to stop the engine when it is deep in the tree of possible moves. I may try it in winter. The key concept of idleAdd() or timeoutAdd() is that the GTK GUI is not directly updated from the second thread, but by a function that is executed in the main thread. You can investigate the Nim Channel module for threading and message passing. I have also added a section to the README: |
@StefanSalewski i'm having this error (my code, your code works fine)
Is your idleAdd from glib or other module? i'm using Code:
|
Nevermind. I completed the code without using |
idleAdd() and timoutAdd() are macros in gintro, the macro then calls the glib g_idle_add(). Macro is needed because of the different calling conventions, g_idle_add() wants to call a function with cdecl calling convention, but our Nim functions generally use nimcall, and we do not want to force the user to mark his user function with cdecl pragma. So we use some sort of trampoline function, that trampoline function is generated by the macro. grep -A40 idleAdd ~/.nimble/pkgs/gintro-#head/gintro/gimpl.nim
macro idleAdd*(p: untyped; arg: typed): untyped =
var IdleID {.compileTime, global.}: int
inc(IdleID)
let ats = $getTypeInst(arg).toStrLit
let procName = "idlefunc_" & $IdleID
let procNameCdecl = "idlefunc_cdecl_" & $IdleID
var r1s = """
proc $1(p: pointer): gboolean {.cdecl.} =
let a = cast[$3](p)
result = $2(a).gboolean
#when (a is ref object) or (a is seq):
#GC_unref(a)
""" % [$procNameCdecl, $p, ats]
let r2s ="""
proc $1(a: $2): culong {.discardable.} =
when (a is ref object) or (a is seq):
GC_ref(a)
return g_idle_add_full(PRIORITY_DEFAULT_IDLE, $3, cast[pointer](a), nil)
else:
var ar: ref $2
new(ar)
#deepCopy(ar[], a)
ar[] = a
GC_ref(ar)
return g_idle_add_full(PRIORITY_DEFAULT_IDLE, $3, cast[pointer](ar[]), nil)
$1($4)
""" % [$procName, ats, $procNameCdecl, $arg]
result = parseStmt(r1s & r2s) |
I use the same syntax for |
Maybe your problem with idleAdd() is that your doCheckIP() function has an array as parameter. I think I never tested with arrays. Maybe it would work when you use an object with two string fields?
This one? Yes I can add it to the example directory, but it is not very interesting as you do no communication with the background thread, you only call blocking execCmd() and non blocking spawnCommandLineAsync(). Unfortunately your onClickIsRun() proc is a plain echo, it does not really test if the background process is running. But well maybe for a plain command launcher tool your example is useful, so I will add it to examples. But I think I will not add it to the README, as it is not too interesting. |
Indeed I wonder if it is a good idea to use spawnCommandLineAsync() from glib at all. Does Nim not provide similar functions? Like startProcess() from osproc module? Well I never have used that one, but generally we use what is provided by Nim std lib and only fall back to external libs when necessary. (Similar for string processing, we generally use Nim strutils and not what is provided by glib.) Maybe you can check osproc.startprocess() and tell us why it is inferior to glib.spawnCommandLineAsync. |
Well i think that is problem of extracting arguments maybe. It is like the connect button (i think i opened an issue about it and the solution is use an object). But i decided using Thread without
|
My new problem: Turned out the worker.join is not a good idea because it crashes application after function works. |
More information: The location from gobject that output error pointed
Tracing back, it goes to gtk.nim
|
I think the reason is i call gtk notification inside threading. Well, the logic of how it work is good but do the actual code with threading is crazy |
You told us that spawnCommandLineAsync() was working fine for you, so I have not further investigated idleAdd() macro. That macro is simple, so expanding it for other data types should be not that hard. Have just tested with an ref object, that compiles, but seems to work only with ARC without crash. I am not really surprised, the default GC has problems with data shared between threads. # nim c --threads:on --gc:arc -r thread2.nim
import gintro/[gtk, glib, gobject, gio]
from os import sleep
type
TwoStr = ref object
s1, s2: string
var workThread: system.Thread[void]
var button: Button
proc idleFunc(x: TwoStr): bool =
button.label = x.s1 & x.s2
return SOURCE_REMOVE
proc workProc =
var countdown {.global.} = 25
var x {.threadvar.}: TwoStr
if x == nil:
x = TwoStr(s1: "Nim", s2: "Rust")
while countdown > 0:
sleep(1000)
dec(countdown)
idleAdd(idleFunc, x)
proc buttonClicked(button: Button) =
button.label = utf8Strreverse(button.label, -1)
proc activate(app: Application) =
let window = newApplicationWindow(app)
window.title = "Countdown"
window.defaultSize = (250, 50)
button = newButton("Click Me")
window.add(button)
button.connect("clicked", buttonClicked)
window.showAll
createThread(workThread, workProc)
proc main =
let app = newApplication("org.gtk.example")
connect(app, "activate", activate)
discard app.run
main() Compile with
I am not sure if passing strings between threads works at all with the default GC. Unfortunately I do not know much about threading in Nim. And I have still to investigate the GTask suggestion of Mr Bassi, I am not sure if GTask will work at all for Nim code that contains GC instructions. Generally you should try to always compile with gc:arc as that is deterministic, you get a crash earlier if something is wrong. The default GC may take long to crash, which is bad for testing. About all your other problems, it is hard to guess without having the full code. Maybe you should ask on the forum, there are some people with some GTK and good threading knowledge. |
I solved this by using Channel. The idea is using
And then in the function that is called by
The key here is not closing the channel so program won't crash if user click on button 2nd time. |
I am having an application that have different buttons, each button does 1 job. So with normal way of coding, my code hangs until the callback completes running. I checked on google and i saw something about multi threaded gtk app:
So i'm having 2 solutions:
So by creating this issue i want to ask does gintro support multi threaded by GTK and is there any example because i can't find any IP by grepping proc's names
The text was updated successfully, but these errors were encountered: