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.