Share and modify struct from master in worker

I am developing a GUI using CImGui.jl and want to run it in a separate process and communicate to/from the master process by modifying a struct. The struct contents could be anything: Array, Strings, file handles.
I cannot return anything from the remote process because CImGui runs and refreshes in a while loop, so returning from the function would close the gui.
I will not post the actual gui code, but a very simplified example:

using Distributed
addprocs(1)
@everywhere struct Data
    crt_text
    cond
end
@everywhere gui = Data(["abc"], [true])
# basic test
@everywhere function run_gui()
    cond = true
    while(cond)
        println(gui.crt_text[1])
        gui.crt_text[1] = "xyz"
        sleep(0.5)
        cond = gui.cond[1] #the actual gui will have a CImGUI dedicated close button
    end
    return nothing
end
#check data before spwan
gui.crt_text[1]
@spawnat 2 run_gui()
# stop execution
@everywhere gui.cond[1] = false
# check data after spawn
gui.crt_text[1]

After execution of run_gui() I was expecting gui.crt_text[1] to be xyz, but it was not modified. How can I achieve this? Thank you!

Setting the value

gui.crt_text[1] = "xyz"

lacks a @everywhere, this works as you expected:

@everywhere function run_gui()
    cond = true
    while(cond)
        println(gui.crt_text[1])
        @everywhere gui.crt_text[1] = "xyz"
        sleep(0.5)
        cond = gui.cond[1] #the actual gui will have a CImGUI dedicated close button
    end
    return nothing
end
3 Likes

Thank you very much! I thought that @everywhere should be used only in the master process.

Can you share a link to the docs? Is this somewhere in the docs? May be it is the case, but I don’t know.

https://docs.julialang.org/en/v1/stdlib/Distributed/#Distributed.@everywhere

@everywhere [procs()] expr
Execute an expression under Main on all procs. Errors on any of the processes are collected into a CompositeException and thrown. For example:

@everywhere bar = 1
will define Main.bar on all processes.

Unlike @spawnat, @everywhere does not capture any local variables. Instead, local variables can be broadcast using interpolation:

foo = 1
@everywhere bar = $foo
The optional argument procs allows specifying a subset of all processes to have execute the expression.

Equivalent to calling remotecall_eval(Main, procs, expr).

It’s not clear for me from the docs that one could @everywhere in a worker.

ok, but it doesn’t explicitly exclude it.

An alternative would be to return the altered gui struct:

@everywhere function run_gui()
    cond = true
    while(cond)
        println(gui.crt_text[1])
        gui.crt_text[1] = "xyz"
        sleep(0.5)
        cond = gui.cond[1] #the actual gui will have a CImGUI dedicated close button
    end
    return gui
end

and fetch it after stoping the process:

gui.crt_text[1]
s = @spawnat 2 run_gui()
# stop execution
@everywhere gui.cond[1] = false
# check data after spawn
gui=fetch(s)
gui.crt_text[1]

True. Thanks again for your help!

2 Likes

Since it’s somehow related, I’ll ask for your help once more. First a bit more background about my project:
I’m writing this GUI as a front panel for a lab instrument(let’s say a power supply) for a remote lab session with students (COVID19 online measurements lab…). I want it to display information by query-ing the instrument (ex: what’s the output voltage) and also to be able to configure it by pushing buttons. So far so good, I managed to connect to the instrument and do all the above, but at the moment everything runs in the main process(so it blocks the REPL).

Now comes the tricky part: the student is supposed to send commands to the instrument from REPL and the GUI should update and provide visual feedback from another thread, without blocking the REPL. This means that the instrument handle responsible for instrument communication should be available both in main process(for the student REPL usage) and in the worker(for the GUI update).

This sharing should work simply by using @everywhere. The problem I face is that I can communicate with the instrument from REPL, but whenever I try to refresh my GUI in the worker I get an Invalid handle error.

My current workaround for this is to use Tasks, but I’m afraid user experience will suffer when running 3-4 GUIs, all waiting to refresh while typing in the REPL.

It’s hard to provide and MWE in this case. To make things easier I tried to replicate this behavior using an arduino:

using Distributed
addprocs(1)
@everywhere using LibSerialPort
list_ports()

# open comm
arduino_main = LibSerialPort.open("COM3", 115200)
# make it visible to the worker
@everywhere arduino_worker = $arduino_main

# read some data in main process
readuntil(arduino_main, '\n', 1.0)

# try to read some data in worker
fetch(@spawnat 2 readuntil(arduino_worker, '\n', 1.0))

Communication from main process works fine (returns some 0s, doesn’t matter). Communication from worker fails with:

ERROR: On worker 2:
From C:\Julia 1.5.0\.julia\packages\LibSerialPort\xvkkM\src\wrap.jl: 141:

libserialport returned SP_ERR_ARG - Function was called with invalid arguments.
error at .\error.jl:33
handle_error at C:\Julia 1.5.0\.julia\packages\LibSerialPort\xvkkM\src\wrap.jl:136
sp_input_waiting at C:\Julia 1.5.0\.julia\packages\LibSerialPort\xvkkM\src\wrap.jl:660
bytesavailable at C:\Julia 1.5.0\.julia\packages\LibSerialPort\xvkkM\src\high-level-api.jl:420
readuntil at C:\Julia 1.5.0\.julia\packages\LibSerialPort\xvkkM\src\high-level-api.jl:402
readuntil at C:\Julia 1.5.0\.julia\packages\LibSerialPort\xvkkM\src\high-level-api.jl:386 [inlined]
#1 at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Distributed\src\macros.jl:87
#103 at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Distributed\src\process_messages.jl:290
run_work_thunk at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Distributed\src\process_messages.jl:79
run_work_thunk at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Distributed\src\process_messages.jl:88
#96 at .\task.jl:356
Stacktrace:
 [1] #remotecall_fetch#143 at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Distributed\src\remotecall.jl:394 [inlined]
 [2] remotecall_fetch(::Function, ::Distributed.Worker, ::Distributed.RRID) at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Distributed\src\remotecall.jl:386
 [3] #remotecall_fetch#146 at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Distributed\src\remotecall.jl:421 [inlined]
 [4] remotecall_fetch at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Distributed\src\remotecall.jl:421 [inlined]
 [5] call_on_owner at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Distributed\src\remotecall.jl:494 [inlined]
 [6] fetch(::Future) at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Distributed\src\remotecall.jl:533
 [7] top-level scope at D:\buildbot\worker\package_win64\build\usr\share\julia\stdlib\v1.5\Distributed\src\macros.jl:99

Can someone explain why these particular variables representing communication sessions fail to be shared in a worker (being defined/communication opened in main process), and how to overcome this?

Thank you!

I am not sure, if above code is supposed to work as simple as it is.
From
https://sigrok.org/api/libserialport/unstable/index.html
I see e.g.:

Thread safety

Certain combinations of calls can be made concurrently, as follows.

  • Calls using different ports may always be made concurrently, i.e. it is safe for separate threads to handle their own ports.
  • Calls using the same port may be made concurrently when one call is a read operation and one call is a write operation, i.e. it is safe to use separate “reader” and “writer” threads for the same port. See below for which operations meet these definitions.

and further important reading.
Check you coding details according to the rules of libserialport.

There is also something about
https://docs.julialang.org/en/v1/manual/multi-threading/#Side-effects-and-mutable-function-arguments-1

@threadcall

External libraries, such as those called via ccall , pose a problem for Julia’s task-based I/O mechanism. If a C library performs a blocking operation, that prevents the Julia scheduler from executing any other tasks until the call returns.

I’m afraid that I’m not experienced enough in your specific task to help remotely.