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

server/2024/9/24 20:27:11/

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

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

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

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

Go">package mainimport ("runtime""time"
)func main() {//设置逻辑处理器 P 的数目为 4runtime.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 的数目为 4runtime.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 {//遍历所有的逻辑处理器 Pfor 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 = nowcontinue}//更改逻辑处理器 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 以执行调度程序。


http://www.ppmy.cn/server/106251.html

相关文章

文字滚动通知功能实现 vue 组件

工作中有使用到文字滚动功能&#xff0c;做个demo记录&#xff0c;方便下次复制&#xff0c;分享给大家。 <script setup lang"ts"> import { ref, nextTick } from vue; const noticeList ref([]);const getNoticeList async () > {const list [测试文字…

【MySQL】字符串存储类型比较

MySQL 字符串存储类型比较 1. CHAR 固定长度字符串最大长度为 255 字符存储时右padding空格到指定长度检索时自动删除尾随空格适用于长度固定的短字符串&#xff08;如邮政编码&#xff09; 示例&#xff1a; CREATE TABLE example (id INT, code CHAR(5)); INSERT INTO ex…

html+css网页设计 动漫 海贼王14个页面

htmlcss网页设计 动漫 海贼王14个页面 网页作品代码简单&#xff0c;可使用任意HTML编辑软件&#xff08;如&#xff1a;Dreamweaver、HBuilder、Vscode 、Sublime 、Webstorm、Text 、Notepad 等任意html编辑软件进行运行及修改编辑等操作&#xff09;。 获取源码 1&#x…

ffplay源码分析(二)结构体VideoState

在多媒体的世界里&#xff0c;播放器是离用户最近的一环&#xff0c;它将数字编码的音频和视频数据转化为生动的视听体验。ffplay 播放器作为一款强大而备受关注的工具&#xff0c;其背后隐藏着一系列精妙的结构体&#xff0c;它们协同工作&#xff0c;共同完成了从数据读取、解…

如何给上万张照片打上标签?,提高整理素材效率!

在数字时代&#xff0c;照片和图像已成为我们记录生活、工作和创造的重要方式。然而&#xff0c;随着照片数量的激增&#xff0c;如何有效管理和分类这些照片成为了一个挑战。今天&#xff0c;我要和大家分享一个高效的解决方案——利用AI技术给照片打标签&#xff0c;从而提高…

Qt WebSocket

简介 WebSocket 是一种网络传输协议&#xff0c;可在单个 TCP 连接上进行全双工通信&#xff0c;位于 OSI 模型的应用层。允许服务端主动向客户端推送数据。 在 WebSocket API 中&#xff0c;浏览器和服务器只需要完成一次握手&#xff0c;两者之间就可以创建持久性的连接&am…

【Java】—— Java面向对象基础:Person类实例操作

目录 一、定义Person类 二、创建Person对象并操作 三、理解对象之间的关系 四、总结 在Java编程中&#xff0c;面向对象编程&#xff08;OOP&#xff09;是一种非常核心且广泛使用的编程范式。它允许我们通过类&#xff08;Class&#xff09;来定义对象的属性和行为&#x…

面试题目:(6)翻转二叉树

题目 翻转二叉树 &#xff08;中间对称翻转&#xff0c;等于镜像&#xff09;给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。示例1&#xff1a; 输入&#xff1a;root [4,2,7,1,3,6,9]输出&#xff1a;[4,7,2,9,6,3,1]示例1&#xff1…