英文:
golang: how to handle blocking tasks optimally?
问题
众所周知,goroutine是一种同步但非阻塞的处理单元。Golang调度器能够很好地处理非阻塞任务,例如套接字、定时器、信号或其他来自字符设备的事件。
但是对于阻塞设备IO或CPU敏感的任务,情况如何呢?它们无法被中断,直到完成,并且不能进行多路复用。运行goroutine的操作系统线程将会冻结,直到goroutine返回或让出控制。在这种情况下,调度粒度变得很差。
当然,你可以在代码中将任务分解为较小的子任务,例如不要一次性复制1GB的文件,而是先复制10MB,让出控制,然后再复制另外10MB,以此类推,这样同一操作系统线程中的其他goroutine就有机会运行。对于CPU密集型任务,也可以采用类似的方法:将文件分成多个部分进行压缩,最后再合并。
但是这样会破坏顺序编程的便利性,并且手动调度很难均匀估计,与操作系统线程上的操作系统调度相比。
nginx也存在类似的问题,它是一个多工作进程程序,每个进程对应一个CPU核心,类似于GOMAXPROCS的最佳实践。它引入了线程池来处理阻塞任务。也许这对于Golang来说也是一个不错的选择。
我很好奇为什么Golang没有操作系统线程的API,这应该是对于处理阻塞任务的goroutine的一个很好的补充。
英文:
As known, the goroutine is synchronous but non-blocking processing unit.
The golang scheduler handles the non-blocking task, e.g. socket, timer, signal or other events from char devices very well.
But how about block device io or CPU sensitive task? They couldn't be interrupted until finish, and not multiplexed. The OS thread which runs the goroutine would freeze until the goroutine returns or yields. In that case, the scheduling granularity becomes bad.
Of course, you could split the tasks into smaller sub-tasks in your codes, for example, do not copy 1GB file at one time, instead, copy first 10MB, yield, and copy another 10MB, etc, so that the other goroutines within the same OS thread get chance to run. Another example for CPU-bound task: zip a file part by part and merge them finally.
But that breaks the convenience of sequential programming, and the manual scheduling is hard to estimate evenly, compared to the OS scheduling upon the OS threads.
The nginx has similar issue, it's multi-worker-processes program, one process for one CPU core, similar to the best practice of the GOMAXPROCS. It brings in the thread pool to handle the blocking tasks. Maybe it's good for golang too.
I am curious why golang has no OS threading API, which should be good supplement to goroutine for blocking tasks.
答案1
得分: 6
Go语言选择了M:N线程模型,而不直接将操作系统线程暴露给用户。在Go中,执行的单位是goroutine,它将在N个操作系统线程上进行多路复用。
在极少数情况下,如果你有一个CPU密集型计算,其中不包含抢占点并且没有足够的操作系统线程来继续运行其他goroutine,你有两个选择:增加GOMAXPROCS或插入runtime.Gosched()
调用以让出给其他goroutine。
在阻塞系统调用的情况下,Go调度器将自动分派一个新的操作系统线程(将系统调用视为“阻塞”的时间限制为20微秒),并且由于非网络IO是一系列阻塞的系统调用,它几乎总是分配给一个专用的操作系统线程。由于Go已经使用了M:N线程模型,用户通常不会意识到底层调度器的选择,并且可以像运行时使用异步IO一样编写程序。
关于使用异步文件IO存在一个未解决的问题,但是需要克服许多问题,比如Linux aio api的不足,跨平台兼容性以及与各种文件系统和设备进行IO的交互。
英文:
Go has specifically chosen to not directly expose OS threads to the user, and instead chose an M:N threading model. Your unit of execution in Go is the goroutine, which will be multiplexed on N number of OS threads.
In the rare case you have a CPU intensive calculation that contains no preemption points and insufficient OS threads to continue running other goroutines, you have 2 choices; increase GOMAXPROCS, or insert runtime.Gosched()
calls to yield to other goroutines.
In the case of blocking syscalls, the Go scheduler will automatically dispatch a new OS thread (the time limit to consider a syscall "blocking" has been 20us), and since non-network IO is a series of blocking syscalls, it will almost always be assigned to a dedicated OS thread. Since Go already uses an M:N threading model, the user is usually unaware of the underlying scheduler choices, and can write the program the same as if the runtime used asynchronous IO.
There is an open issue to consider using asynchronous file IO, but there are many issues to overcome, like shortcomings in the Linux aio api, cross-platform compatibility, and interactions with all the various filesystems and devices with which you can do IO.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论