深入理解 Go 语言并发编程之系统调用底层原理

用户协程是如何执行系统调用的?系统调用有可能会阻塞线程 M,如果所有的线程 M 都因系统调用阻塞了,这时候谁来调度协程呢?

1. 系统调用会阻塞线程吗

系统调用会阻塞线程吗?在这回答这个问题之前,我们先模拟一个 Go 程序执行阻塞式系统调用的情况。

第一个程序就是普通的 Go 程序,没有执行任何系统调用,代码如下所示:

Go 复制代码
package main

import (
	"runtime"
	"time"
)

func main() {
	//设置逻辑处理器 P 的数目为 4
	runtime.GOMAXPROCS(4)
	//创建 10 个用户协程
	for i := 0; i < 10; i++ {
		go func() {
			for {
			}
		}()
	}
	time.Sleep(time.Second * 1000)
}

上面的 Go 程序非常简单,本身没有任何意义,只是显式地设置逻辑处理器 P 的数目为 4,随后创建了多个用户协程。

参考上面的结果,Go 程序总共创建了 5 个子线程,暂时记录下这个结果,待会和第二个程序的输出做一个对比。当然,你可能会好奇为什么实际的线程数大于逻辑处理器 P 的数目,这里我们大不必纠结背后的原因(Go 语言还会创建辅助线程)。

第二个程序会执行一些阻塞式的系统调用,代码如下所示:

Go 复制代码
package main
import (
	"net"
	"runtime"
	"syscall"
	"time"
)

func main(){
	//目的 IP 地址
	var addr = "x.x.x.x"
	//设置逻辑处理器 P 的数目为 4
	runtime.GOMAXPROCS(4)
	for i :=0; i <10;i++{
		go func(){
			sa := ipv4Sockaddr(addr)
			fd,_:=syscall.Socket(syscall.AF_INET,syscall.SOCK_STREAM,syscall.IPPROTO_TCP)
			//阻塞式系统调用
			_= syscall.Connect(fd,sa)
		}()
		//普通的用户协程
		go func(){
			for {
				
			}
		}()
	}
	time.Sleep(time.Second*1000)
}

//解析IP地址
func ipv4Scockaddr(addr string) syscall.Sockaddr {
	ip := net.ParseIP(addr)
	sa := &syscall.SockaddrInet4{Port:80}
	copy(sa.Addr[:],ip.To4())
	return sa
}

上面的程序稍微有点复杂。同样,显式地设置逻辑处理器 P 的数目为 4,并且创建了多个用户协程,只是部分协程执行了一些阻塞式系统调用。注意,这里使用了原生的套接字相关系统调用,并没有使用 Go 语言基于 I/O 多路复用封装的函数(这些函数都是非阻塞调用,不会阻塞线程 M)。函数 syscall.Connect 对应的就是系统调用 connect,用于向远端地址发起 TCP 连接请求,那如果目的 IP 不存在或者与当前节点网络不通呢?这样是不是就会长时间阻塞线程 M 了呢?编译并运行 Go 程序,同时通过 pstree 命令查看该进程所有的线程,结果如下所示:

参考上面的结果,这一次 Go 程序总共创建了 15个子线程,对比第一个程序的输出结果,多创建了 10 个子线程,为什么会多创建这么多子线程呢?是因为用户协程执行了系统调用 syscall.Connect,导致线程 M 阻塞了,Go语言才创建了更多的线程 M 吗?有可能是。

再次回到最初的问题,系统调用阻塞线程 M 之后,Go 语言是如何处理的?参考上面两个程序的输出结果,我们是不是能猜测Go语言会创建更多的线程 M 用于调度协程?只是,线程 M 需要绑定逻辑处理器 P 才能调度协程,所以可想而知,Go语言还需要解除因系统调用而阻塞的线程与逻辑处理器 P 之间的绑定关系。

2. 系统调用与调度器

思考一下,既然系统调用有可能会阻塞线程 M 这一事实无法改变,那么在执行可能阻塞

Go 复制代码
//辅助线程 sysmon 每 10ms 执行一次该函数
func retake(now int64) uint32 {
	//遍历所有的逻辑处理器 P
	for i := 0;i< len(allp); i++{
		if s==_Psyscall {
			//如果不等于,说明系统调度已结否
			t := int64(_p_.syscalltick)
			if !sysretake && int64(pd.syscalltick) != t {
				pd.syscalltick = uint32(t)
				pd.syscallwhen = now
				continue
			}
			//更改逻辑处理器 P 的状态
			if atomic.Cas(&_p_.status,s,_Pidle){
				//创建新的线程 M 以执行调度程序
				handoffp(_p_)
			}
		}
	}
}

参考上面的代码,逻辑处理器 P 有两个变量:变量 syscalltick 用于计数,每执行一次系统调用都会加 1 ;变量 syscallwhen 记录最近一次系统调用的执行时间,当然这个时间其实不是真正的执行时间,可以理解为检测时间。检测的整体逻辑是,如果逻辑处理器 P 处于系统调用状态(_Psyscall),并且自从上次检测之后计数器 syscalltick 没有发生变化,说明当前逻辑处理器 P 近 10ms 内一直处于系统调用状态,即与其绑定的线程 M 近 10ms 内一直处于阻塞状态。此时,辅助线程就会将逻辑处理器 P 的状态修改为空闲状态(_Pidle),并调用函数 runtime.handoffp 创建新的线程 M 以执行调度程序。

相关推荐
DanielYQ8 分钟前
LCR 001 两数相除
开发语言·python·算法
yngsqq13 分钟前
037集——JoinEntities连接多段线polyline和圆弧arc(CAD—C#二次开发入门)
开发语言·c#·swift
Zԅ(¯ㅂ¯ԅ)15 分钟前
C#桌面应用制作计算器进阶版01
开发语言·c#
过期的H2O217 分钟前
【H2O2|全栈】JS进阶知识(七)ES6(3)
开发语言·javascript·es6
一路冰雨27 分钟前
Qt打开文件对话框选择文件之后弹出两次
开发语言·qt
松树戈36 分钟前
JS推荐实践
开发语言·javascript·ecmascript
瑞雨溪39 分钟前
java中的this关键字
java·开发语言
MapleLea1f1 小时前
26届JAVA 学习日记——Day14
java·开发语言·学习·tcp/ip·程序人生·学习方法
小汤猿人类1 小时前
SpringTask
开发语言·python
爪哇学长1 小时前
解锁API的无限潜力:RESTful、SOAP、GraphQL和Webhooks的应用前景
java·开发语言·后端·restful·graphql