0%

go的线程何时会阻塞

什么是协程

OS 并不理解 协程,协程是在 userspace 模拟出来的调度,协程运行在线程之上,所以协程没有上下文切换消耗。

Go什么时候会阻塞调用线程?

太长不看版:

Go 进行系统调用时,如果 OS 对于 socket,file 的 文件描述符fd 不支持 IO multiplxing,Go 会阻塞调用线程。

我们都知道,goroutine 是在线程上模拟调度出来的,那在什么情况下,go runtime 会再创建新的线程?

我们从最基础的概念开始。

进程的系统调用是以线程为单位的,而 goroutine 是在线程之上,user space 模拟出的调度。当 Go code 进行系统调用时,code 当前运行的线程会进行 syscall,从而使得调用线程被阻塞。操作系统不理解 goroutine,只能感知到 线程,所以可以理解为,当进行系统调用时,线程就是 Go 押在操作系统的人质(不押韵)

对于 socket 的读写操作都属于系统调用。

设想一下这个场景,我们开发了一个 http server, 每个连接都使用新的 goroutine 去处理,那当我们有 1000 个 connections,线程数应到 1000+(并且大部分都阻塞),但这不合实际。实际上,Go 对于 socket 实现了高效了 IO多路复用。

blocking thread 转换为 blocking goroutine 的设计叫做 netpoller

netpollergoroutine 接受事件,然后将其分发到操作系统提供的IO处理器。

Linux 中使用 epollBSD, Drawin 下使用 kqueueWindows下使用 IOCP

https://morsmachine.dk/netpoller

netpoll 是为 socket,那对于普通的文件呢?

再次设想一下:

我们对磁盘中 10000 个文件进行读写,如果不使用 poll 技术,我们需要阻塞至少 10000 个线程,这简直无法接受。

这个问题在 Github Issues 上有很多的讨论。

你可以在最后的参考中找到链接

为什么 Go 只为 socket 使用 poll 技术呢?

Windows 下的 IOCP 支持 socket, file

Linux 下的 epoll 只支持 socket

所以 正是由于不同平台对于 poll 技术支持特性的不同,使得 Go 在读写文件时会采用阻塞线程的无奈之举。

直到反应的人越来越多,Go 最终在 这个PR 中完善了处理

那如何解决的呢?

我们用读取文件举例子。

windowsReadFile 实现 runtime/syscall_windows.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func ReadFile(handle Handle, buf []byte, done *uint32, overlapped *Overlapped) (err error) {
var _p0 *byte
if len(buf) > 0 {
_p0 = &buf[0]
}
// 这里实际上调用了 ReadFile 这个 Windows 的API
r1, _, e1 := Syscall6(procReadFile.Addr(), 5, uintptr(handle), uintptr(unsafe.Pointer(_p0)), uintptr(len(buf)), uintptr(unsafe.Pointer(done)), uintptr(unsafe.Pointer(overlapped)), 0)
if r1 == 0 {
if e1 != 0 {
err = errnoErr(e1)
} else {
err = EINVAL
}
}
return
}

Windows下关于 ReadFile API 的文档

1
This function(ReadFile) is designed for both synchronous and asynchronous operations.

现在 Go 统一对 socket,file 的 文件描述符 fd 做了异步处理。

Linux 下,由于 epoll 不支持 对于文件的读写,所以 Go 像之前那样阻塞调用线程。

如果 goroutine 进行了阻塞式系统调用,那么当前线程会被阻塞,当 go 调度器没有足够的线程可以支持其余的 goroutine 时,调度器会创建额外的线程

runtime.LockOSThread 是做什么呢?

还是到了 goroutinethread 的区别。

操作系统并不理解协程,协程是在 userspace 中模拟调度出来的。

既然 OS 不理解协程,Go 进行系统调用时,必须以线程为质押。

go scheduler 并不保证 goroutine 始终在同一线程运行。所以为了确保系统调用能够成功,进行 syscallgoroutine(A) 必须固定在某个线程运行,调用 LockOSThread 之后,其余的 goroutine 不能在这个线程运行。除非 A 调用 UnlockOSThread

https://stackoverflow.com/questions/25361831/benefits-of-runtime-lockosthread-in-golang/25362395#25362395

参考来源:

  1. https://github.com/golang/go/commit/c05b06a12d005f50e4776095a60d6bd9c2c91fac
  2. https://github.com/golang/go/issues/18507
  3. https://github.com/golang/go/issues/6222
  4. https://github.com/golang/go/issues/6817
  5. https://stackoverflow.com/questions/8057892/epoll-on-regular-files
  6. https://morsmachine.dk/netpoller