Skip to content
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

Can Weave spawn a thread that sends a Channel message to the main channel without blocking the main thread? #198

Open
PhilippMDoerner opened this issue Dec 19, 2023 · 3 comments

Comments

@PhilippMDoerner
Copy link

Heyho, I am trying to get an example to work where I spawn a thread to perform a task in weave that eventually sends a Channel message, while the proc itself returns nothing.

I want to go for a Channel message as I want a setup with a thread running an event-loop of a GUI that regularly reads messages send to it from possibly multiple other "Task"-threads.

It should not ever block waiting for any of the "Task"-threads to finish as it should run its own event-loop and stay responsive to user-input.

The code would look roughly like this (if spawn only spawned a single thread for that one proc call):

import weave
import std/os

var chan: Channel[string]
chan.open()

proc doThing() {.gcsafe.} =
  sleep(1000) # Very compute intensive task or one where you waitFor an async operation like an HTTP request
  chan.send("Sending")

proc guiLoop() =
  var counter: int

  while true: # A GUI loop
    let resp = chan.tryRecv
    counter.inc
    if resp.dataAvailable:
      echo resp.msg
      echo "Counter: ", counter
      break
    
    sleep(10)

proc main() =
  init(Weave)
  
  spawn doThing()

  guiLoop()
    
  exit(Weave)

main()

Now obviously the above does not work.
I'm not entirely sure what it does, but I'm very sure it spawns more than 1 thread as it maxes out my 16 core CPU on top of never even starting to execute doThing.

Is there a way to express that with weave?

I initially assumed that this fell under "task parallelism" since I would want to do 2 tasks in parallel (run gui thread, execute "doThing"), but I'm really not that knowledgeable with the terminology thrown around.

@PhilippMDoerner PhilippMDoerner changed the title Can Weave spawn a thread that sends a Channel message back without blocking the main thread? Can Weave spawn a thread that sends a Channel message to the main channel without blocking the main thread? Dec 19, 2023
@mratsim
Copy link
Owner

mratsim commented Dec 30, 2023

Weave provides isReady, see nim-lang/RFCs#347 (comment).

That said Weave internals use "work-requesting" instead of "work-stealing" so load balancing require cooperation between threads. If you offload something that blocks, for example that does IO like writing to a file or stdout, you may block the whole runtime.

Also I've added an experimental API called submit to allow submitting tasks from any thread to improve interop with async, and ensure execution is done on a thread independent from async, see https://github.com/mratsim/weave/blob/master/rfcs/multithreading_apis.md#experimental-non-blocking-task-parallelism-api.

I do not think it's the best way forward, ideally we have a threadpool built for IO, but it at least exist.

@PhilippMDoerner
Copy link
Author

PhilippMDoerner commented Dec 30, 2023

If I understand approaches using isReady correctly, that would require me to manage any task I create myself, throughout the multiple iterations of the while-loop that may occur on Thread A between a task being spawned on Thread A and it finishing on Thread B.
E.g. I'd need to have a thread-local queue for Thread A into which it puts all tasks that it generates, and at the end of each loop It checks every task in the queue if it's ready. If it is, then remove it from the queue, take its result and handle it with an appropriate proc.

That's doable, but sounds like considerable overhead. By having a task simply send messages back to the main-thread via a channel I circumvent a significant chunk of that.
A main-thread with an event-loop like here (see guiLoop) is forced to check for messages on its channel regardless since it could be receiving many messages from not just the task, but also other threads (thinking of it like an actor model) etc.

@mratsim
Copy link
Owner

mratsim commented Jan 1, 2024

If I understand approaches using isReady correctly, that would require me to manage any task I create myself, throughout the multiple iterations of the while-loop that may occur on Thread A between a task being spawned on Thread A and it finishing on Thread B. E.g. I'd need to have a thread-local queue for Thread A into which it puts all tasks that it generates, and at the end of each loop It checks every task in the queue if it's ready. If it is, then remove it from the queue, take its result and handle it with an appropriate proc.

That's doable, but sounds like considerable overhead. By having a task simply send messages back to the main-thread via a channel I circumvent a significant chunk of that. A main-thread with an event-loop like here (see guiLoop) is forced to check for messages on its channel regardless since it could be receiving many messages from not just the task, but also other threads (thinking of it like an actor model) etc.

Can't you do the following:

proc foo(ctx: ptr Context, arg0: T, arg1: U, arg2: V): bool =
  # ... heavy processing
  ctx.signalReady() # The context has the taskID

Otherwise your problem is quite similar to the design constraint I had for implementing dataflow parallelism with events or many events that trigger dependent spawns:

block: # Delayed computation with multiple dependencies
proc echoA(eA: FlowEvent) =
echo "Display A, sleep 1s, create parallel streams 1 and 2"
sleep(1000)
eA.trigger()
proc echoB1(eB1: FlowEvent) =
echo "Display B1, sleep 1s"
sleep(1000)
eB1.trigger()
proc echoB2(eB2: FlowEvent) =
echo "Display B2, no sleep"
eB2.trigger()
proc echoC12() =
echo "Display C12, exit stream"
proc main() =
echo "Sanity check 4: Dataflow parallelism with multiple dependencies"
init(Weave)
let eA = newFlowEvent()
let eB1 = newFlowEvent()
let eB2 = newFlowEvent()
spawnOnEvents eB1, eB2, echoC12()
spawnOnEvent eA, echoB2(eB2)
spawnOnEvent eA, echoB1(eB1)
spawn echoA(eA)
exit(Weave)
echo "Weave runtime exited"

You can read the implementation here: https://github.com/mratsim/weave/blob/master/weave/cross_thread_com/flow_events.nim it's multithreading runtime agnostic and only need threadsafe MPSC queues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants