深入理解 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 以执行调度程序。

相关推荐
&岁月不待人&13 分钟前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
StayInLove16 分钟前
G1垃圾回收器日志详解
java·开发语言
无尽的大道24 分钟前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
爱吃生蚝的于勒28 分钟前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
binishuaio37 分钟前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE39 分钟前
【Java SE】StringBuffer
java·开发语言
就是有点傻43 分钟前
WPF中的依赖属性
开发语言·wpf
洋2401 小时前
C语言常用标准库函数
c语言·开发语言
进击的六角龙1 小时前
Python中处理Excel的基本概念(如工作簿、工作表等)
开发语言·python·excel
wrx繁星点点1 小时前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式