How I understand it (but maybe some people will correct me):

I will use the term "Task" to represent any code subroutines, whether that 
subroutine is a stackful Coroutines or a thread.

In the M:N model:

  * **Design 1** : the user creates and execute a Task and execute a Task. The 
runtime decides for the users how the task is run. Generally it runs inside a 
coroutine itself inside another thread. But depending on workload, the runtime 
can move across threads. That's the Go model. _Advantages_ : simple, less code, 
can compensate skill issues from the programmer. _Drawbacks_ : Less flexibility 
and control, much more complex and unsafe
  * **Design 2** : the user creates and execute a Task. The runtime "analyses" 
the code to spawn either a thread or a coroutine. That seems to be your 
proposition. Simpler than the first model, but with still the same drawbacks



In the 1:N model:

  * The user creates a task and tells explicitly if this task is I/O bound or 
CPU bound. But nothing prevents him from spawning an I/O bound task inside a 
CPU task or the reverse. Advantages: Simpler to implement, more control from 
the user. Disavantages: More API than the user need to know (otherwere sharing 
similar concepts "spawn/wait/FlowVar" Vs "goAsync/wait/GoTask" -> FlowVar can 
be wrapped into a GoTask). More code for the end user. More complex for the 
user. More prone to deadlocks. Not one way to do things.



In either models, passing GC variable and using closures is complicated with 
threads. But in M:N model, that impact all `go` functions. Whereas in 1:N, 
those limitations only impact the `goThread` macro.

I do think all the N:M model can do, the 1:N model can also do with a little 
more reflexion or boilerplate. the following examples are possible in 1:N 
model, I let the reader judge if this is difficult or bad API :
    
    
    # Example 1: Nesting I/O task with CPU task
    var myData: DataTypeRef
    
    goAsync proc(myData) =
      ## We are here in main thread
      waitForAll(
          goThread proc(myData) =
                ## myData will be isolated or race condition will happen
                ## We are here in thread 2
                waitForAll(
                    goAsync proc() = doIOStuff(myData), ## Still in thread 2
                    goThread proc() = doCpuStuff(), ## Will be executed in 
thread 3
                ),
            goAsync proc() = doIoStuff() ## Will be executed in main thread
         )
      )
    
    # Example 2: Having a function that is both I/O and cpu intensive, and 
distribute the work accordngly with channels
    goAsync proc() =
        ## There are many possibilities to do the same thing here. We could 
also consume the code inside producer thread by using goAsync there, but that 
would steal some CPU.
         var mychan: GoChannel
         waitForAll(
            goThread proc() = producerCode(mychan), ## Producer is very 
computing intensive
            goAsync proc() = consumerCode(mychan) ## Consumer is very I/O 
intensive, maybe it sends data to sockets
        )
    
    ## Example 3: an example of a highly loaded server:
    proc consumeClient(chan: GoChan[GoSocket])
    
    goAsync proc() =
      var server = newGoSocket(...)
      server.accept(...)
      var allSpawnedTasks: seq[GoTask[untyped]]
      while true:
         ## We will distribute work, max 100 clients by thread
         var chan: GoChan[GoSocket]
         goThread consumeClient(chan)
         for i in 0..100:
           let client = server.listen()
           chan.send(client)
    
    proc consumeClient(chan: GoChan) =
       ## After 100 clients, our worker thread will stop. This is not a problem 
because the taskpool will recycle it. However if thread are not fast enough, 
the pending task queue of the thread pool could grow big
       while not chan.close:
         let client = chan.recv()
         processClient(client) ## Here we only process one client at a time in 
that thread
    
    
    Run

Of course, because we use a threadpool, we will have a limited number of 
threads having each their own I/O event dispatcher. I'm not sure this design is 
more limited, or utterly complex for the user.

Reply via email to