I’ve been trying to work with OpenGL (ModernGL.jl) within the REPL but using a scheduled task to keep rendering continuously to allow interactivity with schedule(Task(renderLoop)).
I experienced some crashes (difficult to reproduce these) and some OpenGL errors (easy to reproduce since they always happens when I follow the same steps).
After some debugging I think the problem happens because the OpenGL code I put in the REPL is interrupted by the renderLoop function that is in the scheduled task. Due to the nature of OpenGL it is not safe to interrupt blocks of OpenGL code to call other OpenGL code and I can confirm that that the scheduled task executed some instructions in the middle of the REPL code.
Of course, I don’t expect that multiple sentences in the REPL code are gonna make the scheduled task wait, but I did expect to don’t see a function call in the REPL being interrupted by the scheduled task.
I “saw” the interruption when the REPL code executed prints or the C function glXGetProcAddress.
My questions are: Seems my guess correct? is this expected behavior? Calls to println and ccall call yield()? Should this be documented?
If my problem is not very clear I can create and paste here the real code, but I hope some pseudocode is enough:
initialize_opengl()
flag = false
function renderLoop()
global flag
opengl_code()
yield() # this is a safe point for executing other OpenGL code
if flag
@error "We are interrupting external OpenGL code, UNSAFE!!!"
end
end
function extra_stuff()
global flag
flag=true
# OpenGL code that mustn't be interrupted by other OpenGL code
flag=false
end
julia> schedule(Task(renderLoop))
julia> extra_stuff()
Error: We are interrupting external OpenGL code, UNSAFE!!!
Context:
I’m using Linux with Julia 1.0.0, I’m completely new to Julia but not so much to OpenGL.
Thank you! Issues aside, I’m having a great time learning and programming in Julia
So, is it expected to see the task switching (yield of the extra_stuff())?
Maybe the crashes were produced by the stack issue, although I don’t understand much this.
However, I don’t think the behavior I’m seeing with the flag and the @error has something to do with the stack. I think this can just be explained by hidden yields. But, of course, I’d be much happier if someone could confirm this.
You should treat Tasks as-if they are being run on different threads, so the usual provisions apply: use locks around critical regions, avoid race conditions, etc.
I made sigatom (and its pair siginterruptable) in Gtk for convenience at marking critical regions—it just de-sugars to a do-block call to acquire and release the relevant lock.
To add to this, and more specifically address you question about whether this is expected - Julia tasks will generally yield on any I/O operation (printing to STDOUT, reading/writing from disk, etc.) but also any time someone’s code calls wait on a Condition. In general you don’t know whether some code you’re calling is going to do something that yields.
I see, I didn’t know about the automatic yielding. I understand the model is similar to Go’s one.
I tried to use a Threads.Mutex to fix it, but I wasn’t able to make it work. I tried to make the code smaller and I ended up with this:
mutex = Threads.Mutex()
function f()
global mutex
lock(mutex)
println("locked")
i=0
while true
i=i+1
yield()
end
end
schedule(Task(f))
yield()
lock(mutex)
println("hello")
unlock(mutex)
This outputs:
locked
hello
Which for me doesn’t make any sense since the mutex is locked on f as shown with the “locked” print. I expected to see the main task blocked forever and no “hello” in the output.
Ok, I found the problem. I was using a Threads.Mutex but that cover different threads but not different tasks within the same thread (which seems very odd to me).
Replacing it by a ReentrantLock fixed everything, although it clearly states that is not thread-safe.
I don’t care much right now, but I think that having a Lock that actually acquires the mutex per task while being thread-safe is super useful.
I’m probably too inexperienced in Julia for this, but I could to try to push this. Do you think it makes sense to add that new lock? What would be the next steps, opening an issue in github, creating a PR?