node.jsgogoroutineevent-loop

Comparison of Nodejs EventLoop (with cluster module) and Golang Scheduler


In nodejs the main critics are based on its single threaded event loop model.

The biggest disadvantage of nodejs is that one can not perform CPU intensive tasks in the application. For demonstration purpose, lets take the example of a while loop (which is perhaps analogous to a db function returning hundred thousand of records and then processing those records in nodejs.)

while(1){
    x++
}

Such sort of the code will block the main stack and consequently all other tasks waiting in the Event Queue will never get the chance to be executed. (and in a web Applications, new users will not be able to connect to the App).

However, one could possibly use module like cluster to leverage the multi core system and partially solve the above issue. The Cluster module allows one to create a small network of separate processes which can share server ports, which gives the Node.js application access to the full power of the server. (However, one of the biggest disadvantage of using Cluster is that the state cannot be maintained in the application code). But again there is a high possibility that we would end up in the same situation (as described above) again if there is too much server load.

When I started learning the Go language and had a look at its architecture and goroutines, I thought it would possibly solve the problem that arises due to the single threaded event loop model of nodejs. And that it would probably avoid the above scenario of CPU intensive tasks, until I came across this interesting code, which blocks all of the GO application and nothing happens, much like a while loop in nodejs.

func main() {
    var x int
    threads := runtime.GOMAXPROCS(0)
    for i := 0; i < threads; i++ {
        go func() {
            for { x++ }
        }()
    }
    time.Sleep(time.Second)
    fmt.Println("x =", x)
}
//or perhaps even if we use some number that is just greater than the threads.

So, the question is, if I have an application which is load intensive and there would be lot of CPU intensive tasks as well, I could probably get stuck in the above sort of scenario. (where db returns numerous amount of rows and then the application need to process and modify some thing in those rows). Would not the incoming users would be blocked and so would all other tasks as well?

So, how could the above problem be solved?

P.S
Or perhaps, the use cases I have mentioned does not make much of the sense? :)


Solution

  • An update (2025-02-17): goroutine preemption is implemented in Go 1.14, so what's written below is sort of obsolete. Still left for historical interest.

    Currently (Go 1.11 and earlier versions) your so-called tight loop will indeed clog the code. This would happen simply because currently the Go compiler inserts code which does "preemption checks" («should I yield to the scheduler so it runs another goroutine?») only in prologues of the functions it compiles (almost, but let's not digress). If your loop does not call any function, no preemption checks will be made.

    The Go developers are well aware of this and are working on eventually alleviating this issue.

    Still, note that your alleged problem is a non-issue in most real-world scenarious: the code which performs long runs of CPU-intensive work without calling any function is rare and far in between.

    In the cases, where you really have such code and you have detected it really makes other goroutines starve (let me underline: you have detected that through profiling—as opposed to just conjuring up "it must be slow"), you may apply several techniques to deal with this: