从操作系统层面理解同步、异步、阻塞、非阻塞

2023-11-17

同步和异步描述调用者会不会主动等待函数的返回值,举个例子:

public void method() {
	int result = otherMethod();
}

像上面这种形式就叫同步,result 会一直等待 otherMethod() 方法执行完毕并拿到返回值

public void method() {
	new Thread(() -> {
		int result = otherMethod();
	});
}

像上面这种形式就叫异步,主线程不会等待 otherMethod() 方法执行完,一般情况下异步处理会配合回调通知调用方执行结果,回调的方式有很多:回调方法、发送 MQ、调 RPC 接口都可以实现,当然没有回调处理也没问题

总得来说,同步和异步关注调用方是否等待函数的返回值,只要不等待就是异步


阻塞描述的是方法本身在等待某一事件的结果时,是将线程阻塞还是立即返回一个未就绪的信息

某个方法阻塞一定包含某种阻塞操作,I/O 读、写就是最常见的阻塞操作。下面写一段伪代码:

public int read() {
	while (磁盘未就绪) {
		线程阻塞,直到磁盘就绪
	}
	从磁盘中读取数据
	return 读取到的字节数;
}

如上方法就是一个阻塞方法,线程在判断磁盘未就绪时会主动阻塞,直到磁盘就绪

public int read() {
	while (磁盘未就绪) {
		return -1;
	}
	从磁盘中读取数据
	return 读取到的字节数;
}

如上方法就是一个非阻塞方法,线程在判断磁盘未就绪时会直接返回,不会阻塞,不会让出 CPU 资源

也就是说,阻塞关注的是方法本身,包含阻塞调用就是阻塞方法,否则就不是阻塞方法


也就是说同步、异步 和 阻塞、非阻塞没有关系,两者是不同维度的概念。下面依次介绍每种情况:

  • 同步阻塞:线程调用阻塞方法,由于某种原因方法阻塞,线程阻塞,直到阻塞状态消除,方法执行完毕后拿到返回值
  • 异步阻塞:主线程异步调用阻塞方法,调用方法后主线程去干别的事情,不等待返回结果,异步开启的线程和上面同步阻塞处理相同
  • 同步非阻塞:线程调用阻塞方法,由于某种原因方法没法执行,直接返回失败,此时线程可以考虑循环调用或者去干别的事情
  • 异步非阻塞:主线程异步调用阻塞方法,调用方法后主线程去干别的事情,不等待返回结果,异步开启的线程和上面同步非阻塞处理相同

下面通过买书的场景介绍每种情况,方便理解:

  • 同步阻塞:我去前台买书,老板让我在前台等会他去找,我一直在前台等,直到老板拿来书
  • 异步阻塞:我找了个小弟派他去前台找老板买书,老板让小弟在前台等会他去找,小弟一直在前台等,我去干别的事情
  • 同步非阻塞:我去前台买书,老板去找书并让我先去干别的事
  • 异步非阻塞:我找了个小弟派他去前台找老板买书,老板去找书并让小弟先去干别的事

老板让我去干别的事,我可以选择继续在前台等,也可以选择去干别的事,这块根据代码判断是否循环调用
至于老板找到书之后会不会通知属于回调,异步可以不包含回调机制

很多地方习惯把阻塞和同步混为一谈,认为只要阻塞就是同步,只要同步就是阻塞调用,实际上这是不对的


最后从操作系统层面理解阻塞到底是什么,上面提到阻塞常用在和 I/O 相关的场景,牵扯到 I/O 就需要调用底层系统调用

举个例子:当我们调用 java 的 readLine() 方法时程序会阻塞等待用户输入回车,实际它调用的就是 linux 操作系统中的 read() 方法

linux 将所有系统调用注册在系统调用表中,方法以 sys_ 开头,也就是说,read() 方法实际调用 sys_read() 方法,sys_read() 方法再经过一系列文件操作之后,可以看到如下代码:

if (EMPTY (tty->secondary)) {
	sleep_if_empty (&tty->secondary);
}

static void sleep_if_empty (struct tty_queue *queue) {
	cli ();
	while (EMPTY (*queue))
	  interruptible_sleep_on (&queue->proc_list);
	sti ();
}

void interruptible_sleep_on (struct task_struct **p) {
	current->state = TASK_INTERRUPTIBLE;
	schedule ();
}

简单总结就是说:java 程序调用 readLine() 方法最终调用 linux 系统调用 sys_read() 方法,在该方法最后只要用户不输入回车就调用 interruptible_sleep_on() 方法将进程的状态修改为 TASK_INTERRUPTIBLE,即可中断的状态,并调用 schedule() 方法

根据 java 线程模式来看,java 线程采用 1 对 1 操作系统进程的方式实现,一个 java 线程实际就对应 linux 一个轻量级进程,所以下文在操作系统层面均以进程来介绍,大家知道就好

schedule() 方法会强制执行一次进程调度,进程调度主要做三件事:

  1. 拿到剩余时间片(counter)最大并且处于 runnable 状态的进程号 next
  2. 如果所有 runnable 状态进程的时间片都为 0,重新设置所有进程(各种状态)的 counter 值,继续执行步骤 1
  3. 拿到进程号 next,调用 switch_to(next) 方法,切换到对应进程执行

也就是说,进程调度时只会跳转到处于 runnable 状态的进程,而阻塞的进程处于 TASK_INTERRUPTIBLE 状态,一定不会被调度,所谓阻塞就是通过这种方式告诉进程调度,该进程放弃 CPU 的执行权,进程被阻塞

在底层操作系统层面,进程由于处于 TASK_INTERRUPTIBLE 状态放弃 CPU 调度权,而对于上层应用来说线程一直停在那里,就好像挂起了一样,这也就是阻塞的本质


有挂起就有恢复,对于 readLine() 方法,当我们按下回车键后,触发键盘中断,执行一系列中断方法后最终来到以下代码:

wake_up(&tty->secondary.proc_list);

void wake_up(struct task_struct **p)
{
    if (p && *p) {
        (**p).state = TASK_RUNNABLE;
        *p = NULL;
    }
}

很明显,将进程的状态又重新修改回 TASK_RUNNABLE,此时该进程又可以被 CPU 调度了,也就是说进程恢复了

最后总结一下,所谓线程阻塞和恢复到操作系统层面实际都是通过修改进程状态,只有 RUNNABLE 状态的进程才能被操作系统调度,否则就像不动了一样,永远无法获取 CPU 资源,一直阻塞。一句话:挂起、恢复都是通过修改进程状态实现,其它交给进程调度


参考:微信公众号《低并发编程》

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从操作系统层面理解同步、异步、阻塞、非阻塞 的相关文章

随机推荐

  • 记录自己开发入职第一天需要干的事情

    拉取代码前的准备工作 安装JDK 数据库本地可视化客户端 如Navicat SQLyog DBeaver Workbench Windows连接linux终端工具 Xshell SecureCRT Putty 下载IDEA 配置IDEA开发
  • adb shell 获取手机分辨率

    使用adb修改屏幕像素密度 此命令针对全志开发板子 adb shell am display density 120 以下命令针对高通的开发板子 获取Android设备屏幕分辨率 adb shell wm size获取android设备屏幕
  • Tomcat简介及优化思路

    Tomcat 处理请求大致流程和架构 请求流程 1 HTTP服务器会把请求信息使 ServletRequest对象封装起来 2 进 步去调 Servlet容器中某个具体的Servlet 3 在 2中 Servlet容器拿到请求后 根据URL
  • 超分辨率重建测试(DASR)

    测试链接 GitHub LongguangWang DASR CVPR 2021 Unsupervised Degradation Representation Learning for Blind Super Resolutionhttp
  • vue3项目实战---知乎日报----项目搭建

    目录 基础框架和响应式布局 项目介绍 接口文档 vue config pagejson 初始化公共样式 vuex模块 路由模块 utils公共类库 axios 二次封装 响应式处理 vant ui组件库 基础框架和响应式布局 项目介绍 知乎
  • 基于VMWare 16虚拟机 CentOS7linux系统SFTP服务器的搭建与数据上载传输

    文章目录 写在前面 1 SFTP协议 2 SFTP服务器搭建 2 1 windows中SFTP服务器搭建 2 2 linux系统的SFTP服务器搭建 2 2 1 通过su命令 进入管理员权限 2 2 2 创建sftp组 2 2 3 创建一个
  • Vulnhub-DC-1实战靶场

    目录 前言 一 环境搭建 1 准备工具 2 靶场准备 二 渗透过程 1 信息收集 探测目标IP地址 探测目标IP开放端口 网页信息收集 2 漏洞查找与利用 2 1弱口令 2 2Drupalgeddon2 3 Getshell 3 1交互式s
  • Android mtk 添加app编译进系统

    1 根目录下device 搜索devices mk find iname device mk 找出device mediatek common device mk文件添加 endif Add for mediaextractor proce
  • Maven 生成source 文件 Jar

    TOC Maven 生成source 文件 Jar Maven 生成source 文件 Jar Add maven source plugin in your pom xml file
  • win11 vmware 显示 “未能启动虚拟机“ 解决办法

    编辑虚拟机文件 xxx vmx 修改其中的一行内容 virtualHW version 18
  • jQuery操作css方法

    目录 一 jQuery可以使用css方法来修改元素样式 二 设置类的样式方法 1 添加类 2 移除类 3 切换类 三 类操作与className区别 四 显示隐藏效果 1 显示语法 2 隐藏语法 3 切换语法 五 滑动效果 1 上滑效果语法
  • sql注入之报错注入

    报错注入 报错注入在没法用union联合查询时用 但前提还是不能过滤一些关键的函数 报错注入就是利用了数据库的某些机制 人为地制造错误条件 使得查询结果能够出现在错误信息中 这里主要记录一下xpath语法错误和concat rand gro
  • 某**集团夺旗赛的一道隐写题

    解压压缩包 解压出来一个文件file 使用file命令进行查看 发现是data 010查看也无果 看到标题是logistic联想到是否与xor文件有关 遂使用工具xortool 工具在此 xortool file 选概率最大的那个 13 x
  • HDFS API操作的访问方式及JUnit测试类的使用

    HDFS API操作的访问方式 主要分为使用文件系统访问方式和URL访问方式 package com wyg hdfs import java io File import java io FileOutputStream import j
  • hive 高级分组聚合(grouping sets cube和rollup)

    1 grouping sets 1 1 select a b sum c from tbl group by a b grouping sets a b 相当于 select a b sum c from tbl group by a b
  • Python求1-100所有奇数和的方法!

    在之前的文章中 老男孩IT教育小编为大家介绍过Python的特点 优势 用途以及薪资待遇等知识 而为了帮助大家更好的掌握Python 小编将为大家讲解一些实战案例 比如 Python中如何求1 100的奇数和 接下来我们来看看吧 Pytho
  • Stable Diffusion安装教程、model导入教程以及精品promt指令

    文章目录 引言 原理 图片感知压缩 潜在扩散模型 安装 插件 插件与模型下载 常用promt关键字 交流讨论 引言 最近大火的AI作画吸引了很多人的目光 AI作画近期取得如此巨大进展的原因个人认为有很大的功劳归属于Stable Diffus
  • 读取sftp服务器上的文件内容到指定的数据库表内

    引入sftp jar依赖
  • 一些常用的公共js方法

    读者可能会觉得节流与防抖有点像 其实仔细斟酌就能发现他们的不同 节流是指对于连续触发的事件 每隔一段固定时间执行一次 只要事件持续出发就可以执行很多次 在节流里涉及的时间主要是指事件执行的间隔时间 防抖则是对连续触发的事件 只会执行一次 不
  • 从操作系统层面理解同步、异步、阻塞、非阻塞

    同步和异步描述调用者会不会主动等待函数的返回值 举个例子 public void method int result otherMethod 像上面这种形式就叫同步 result 会一直等待 otherMethod 方法执行完毕并拿到返回值