在前后端程序设计开发工作中,小伙伴们一定都接触过事件、异步这些概念。出现这些概念的原因之一是,我们的代码在执行过程中所涉及的逻辑在不同的场合下执行时间的期望是各不相同的。为了尽量做到充分利用CPU等资源做尽可能多的事,免不了通过异步和事件机制的配合来实现系统资源分时复用的效率最大化。相信这个时候后端开发同学肯定会说,我们多线程、协程等并发编程的概念和机制都流行很久了,但大家有没有思考过,服务端各种语言比如golang, JAVA等已经在语言层面帮大家做了相当多的系统底层封装工作。抽象到系统层面,相信大家都知道大名鼎鼎的epoll机制,其核心目标还是实现系统资源分时复用的效率最大化。下面就让我们一起来看看,前后端应用开发场景中异步和事件机制有什么异同吧。
相信前端同学对异步和事件机制会更加敏感,这主要是因为JavaScript的特性导致异步和事件成了语言学习中的必会核心知识点之一。
作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。若以多线程的方式操作这些 DOM,则可能出现操作的冲突。假设有两个线程同时操作一个 DOM 元素,线程 1 要求浏览器删除 DOM,而线程 2 却要求修改 DOM 样式,这时浏览器就无法决定采用哪个线程的操作。当然,我们可以为浏览器引入“锁”的机制来解决这些冲突,但这会大大提高复杂性,所以 JavaScript 从诞生开始就选择了单线程执行。
另外,因为 JavaScript 是单线程的,在某一时刻内只能执行特定的一个任务,并且会阻塞其它任务执行。那么对于类似 I/O 等耗时的任务,就没必要等待他们执行完后才继续后面的操作。在这些任务完成前,JavaScript 完全可以往下执行其他操作,当这些耗时的任务完成后则以回调的方式执行相应处理。这些就是 JavaScript 与生俱来的特性:异步与回调。
当然对于不可避免的耗时操作(如:繁重的运算,多重循环),HTML5 提出了Web Worker,它会在当前 JavaScript 的执行主线程中利用 Worker 类新开辟一个额外的线程来加载和运行特定的 JavaScript 文件,这个新的线程和 JavaScript 的主线程之间并不会互相影响和阻塞执行,而且在 Web Worker 中提供了这个新线程和 JavaScript 主线程之间数据交换的接口:postMessage 和 onMessage 事件。但在 HTML5 Web Worker 中是不能操作 DOM 的,任何需要操作 DOM 的任务都需要委托给 JavaScript 主线程来执行,所以虽然引入 HTML5 Web Worker,但仍然没有改线 JavaScript 单线程的本质。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
js引擎执行异步代码而不用等待,是因有为有 消息队列和事件循环。
消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。
实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。整个机制如下图所示:
这里有几个概念:事件循环、调用栈(执行站)、微任务、宏任务、事件队列
机制如下图所示:
其中上图中的web api和对应的queue在实际应用场景对应两个:微任务和微任务队列、宏任务和宏任务队列,微任务和宏任务的定义包含:
不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同(宏任务)的Event Queue。而Promise和process.nextTick会进入相同(微任务)的Event Queue。
1. 「宏任务」、「微任务」都是队列,一段代码执行时,会先执行宏任务中的同步代码。
2. 进行第一轮事件循环的时候会把全部的js脚本当成一个宏任务来运行。
3. 如果执行中遇到setTimeout之类宏任务,那么就把这个setTimeout内部的函数推入「宏任务的队列」中,下一轮宏任务执行时调用。
4. 如果执行中遇到 promise.then() 之类的微任务,就会推入到「当前宏任务的微任务队列」中,在本轮宏任务的同步代码都执行完成后,依次执行所有的微任务。
5. 第一轮事件循环中当执行完全部的同步脚本以及微任务队列中的事件,这一轮事件循环就结束了,开始第二轮事件循环。
6. 第二轮事件循环同理先执行同步脚本,遇到其他宏任务代码块继续追加到「宏任务的队列」中,遇到微任务,就会推入到「当前宏任务的微任务队列」中,在本轮宏任务的同步代码执行都完成后,依次执行当前所有的微任务。
7. 开始第三轮,循环往复...
下面举例子来说明
例1
javascript
分析:
console.log('4')已经被放在await语法糖生成的Promise.then里了,而await的等待必须要等后面Promise.then之后才会结束。
1 -> 2 -> 3 -> 4
例2
javascript
分析:
1 -> 2 -> 3 -> 4 -> 5 -> 6
例3
javascript
分析:
1 -> 2 -> 3 -> 4 -> 5 -> 6
例4
javascript
分析:
1 -> 2 -> 3 -> 4 -> 5 -> 7 -> 6 -> 8
例5
javascript
分析:
1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9
例6
javascript
分析:
例7
javascript
分析:
后端的情况会根据语言的不同有细微差异,但核心原理和机制是一致的,这里我们以golang为例进行分析。
在golang中,异步调用的实现和其他编程语言有所不同,golang采用goroutine(协程)的方式实现异步调用。goroutine是一种轻量级的线程,可以在程序中创建多个协程,每个协程都是独立的,并且可以并发执行。
在实际应用中,异步调用常用于以下几个场景:
1. 网络请求
在网络通信中,由于网络状况的不确定性,请求的响应时间可能会非常长,如果采用同步调用的方式,就会造成程序长时间阻塞,影响用户体验。因此,我们可以采用异步调用的方式,在请求之后不必等待响应,而是继续执行其他任务,等到响应到来之后再处理。
2. 文件操作
对于一些文件操作,可能需要进行大量的I/O操作,如读取文件内容、写入文件等。这些I/O操作比较耗时,如果采用同步调用的方式,可能会造成程序阻塞并且效率低下。因此,我们可以采用异步调用的方式,在文件操作需要花费大量时间时,使用goroutine执行任务,不会影响主线程的正常运行。
3. 定时任务
在一些定时任务中,可能需要执行一些比较耗时的操作。如果采用同步调用的方式,可能会影响程序的时间精度和稳定性。因此,我们可以使用异步调用的方式,在主线程执行定时任务的同时,开启goroutine执行具体的操作任务,不会影响程序的精度和稳定性。
在golang中,我们可以使用goroutine和channel来实现异步调用的功能。
1. 使用goroutine实现异步调用
在golang中,开启一个goroutine非常简单,只需要在函数前面加上go关键字即可,例如:
上述代码就是在新的goroutine中执行一个任务。我们来看一个完整的示例代码:
通过上述代码,我们可以看到,程序开启了一个goroutine执行任务,同时主线程也在执行另一个任务。在程序运行过程中,主线程和goroutine可以同时运行,相互不影响。
2. 使用channel实现异步调用
在golang中,channel是goroutine之间通信的一种方式,我们可以使用channel来实现异步调用。我们可以创建一个带有缓冲区的channel,然后在goroutine中执行任务,并将结果通过channel传递给主线程,如下所示
在上述代码中,我们创建了一个带有缓冲区的channel,并在goroutine中执行一个任务,任务的结果通过channel传递给主线程。主线程通过循环读取channel中的数据,当channel关闭时,通过ok变量来判断循环是否结束,从而确保程序能够正常退出。
在服务端中涉及到事件应用主要是发生I/O请求时,而这其中网络I/O在golange中占很重要的比重。
当设备上有数据到达的时候,会给 CPU 的相关引脚上触发⼀个电压变化,以通知 CPU 来处理数据。
也可以把这个叫 硬中断
但是我们知道,cpu运行速度很快,但是网络读取数据会很慢,这时候就会长期占用cpu,导致cpu无法处理其他事件,比如,鼠标移动。
那么在linux中是怎么解决掉这个问题的呢?
linux内核将中断处理拆分开,拆分为了2个部分,一个是上面提到的 硬中断,另外就是 软中断。
第一部分接收到cpu电压变化,产生硬中断,然后只做最简单的处理,然后异步的交给硬件去接收信息到缓冲区。这个时候,cpu就已经可以接收其他中断信息过来了。
第二部分就是软中断部分,软中断是怎么做的呢?其实就是对内存的二进制位进行变更,类似于我们平常写业务常用的到的status字段一样,比如网络Io中,当缓冲区接收数据完毕,会将当前状态改为完成。举个例子,epoll读取某个io时间读取完数据时,并不会直接进入就绪态,而是等下次循环遍历判断状态,才会将这个fd塞入就绪列表(当然,这个时间很短,不过相对于cpu来说,这个时间就很长了)。
2.4 以后的内核版本采⽤的下半部实现⽅式是软中断,由 ksoftirqd 内核线程全权处理。和硬中断不同的是,硬中断是通过给 CPU 物理引脚施加电压变化,⽽软中断是通过给内存中的⼀个变量的⼆进制值以通知软中断处理程序。
这也就是为什么知道2.6才有epoll(正式引入)使用的原因,2.4以前内核都不支持这种方式。
总体的数据流转图如下:
一个数据从到达网卡,要经历以下步骤才会完成一次数据接收:
这里的poll函数是说注册的回调函数,在软中断中进行处理的。比如epoll程序,会注册一个“ep_poll_callback”
以go epoll为例:
go: accept –> pollDesc.Init -> poll_runtime_pollOpen –> runtime.netpollopen(epoll_create) -> epollctl(EPOLL_CTL_ADD)
go: netpollblock(gopark),让出cpu->调度回来,netpoll(0)将协程写入就绪态->其他操作…
epoll thread: epoll_create(ep_ptable_queue_proc,注册软中断到ksoftirqd,将方法ep_poll_callback注册到)->epoll_add->epoll_wait(ep_poll让出cpu)
core: 网卡接收到数据->dma+硬中断->软中断->系统调度到ksoftirqd,处理ep_poll_callback(这里要注意,新的连接进入到程序,不是通过callback,而是走accept)->获取到之前注册的fd句柄->copy网卡数据到句柄->根据事件类型,对fd进行操作(就绪列表)
部分代码
go: accept
epoll用kmem_cache_create(slab分配器)分配内存用来存放struct epitem和struct eppoll_entry。
当向系统中添加一个fd时,就创建一个epitem结构体,这是内核管理epoll的基本数据结构:
而每个epoll fd(epfd)对应的主要数据结构为:
struct eventpoll在epoll_create时创建。
其中,ep_alloc(struct eventpoll **pep)为pep分配内存,并初始化。
其中,上面注册的操作eventpoll_fops定义如下:
这样说来,内核中维护了一棵红黑树,大致的结构如下:
clip_image002
接着是epoll_ctl函数(省略了出错检查等代码):
这两个函数将ep_ptable_queue_proc注册到epq.pt中的qproc。
执行f_op->poll(tfile, &epq.pt)时,XXX_poll(tfile, &epq.pt)函数会执行poll_wait(),poll_wait()会调用epq.pt.qproc函数,即为ep_ptable_queue_proc。
ep_ptable_queue_proc函数如下:
ep_ptable_queue_proc(ep_poll_callback)其中struct eppoll_entry定义如下:
在ep_ptable_queue_proc函数中,引入了另外一个非常重要的数据结构eppoll_entry。
eppoll_entry主要完成epitem和epitem事件发生时的callback(ep_poll_callback)函数之间的关联。首先将eppoll_entry的whead指向fd的设备等待队列(同select中的wait_address),然后初始化eppoll_entry的base变量指向epitem,最后通过add_wait_queue将epoll_entry挂载到fd的设备等待队列上。
完成这个动作后,epoll_entry已经被挂载到fd的设备等待队列。
由于ep_ptable_queue_proc函数设置了等待队列的ep_poll_callback回调函数。所以在设备硬件数据到来时,硬件中断处理函数中会唤醒该等待队列上等待的进程时,会调用唤醒函数ep_poll_callback。
所以ep_poll_callback函数主要的功能是将被监视文件的等待事件就绪时,将文件对应的epitem实例添加到就绪队列中,当用户调用epoll_wait()时,内核会将就绪队列中的事件报告给用户。
综上可见,虽然前后端中的异步和事件机制所处的语言模型及应用环境、场景各有差异。但其基本原理和原则大同小异,即都是要减少程序阻塞,在保障程序运行结果正确性和可预测性的前提下最大程度分时复用计算机的存储、计算资源。编程人员应该对前后端这些机制都有所了解,在特定的应用场景和需求下时不时的换位思考才能设计出前后端都能快速正确响应且高效完成的技术方案。
了解我们更多?欢迎进线咨询:
电话:4000052360
邮箱:g-zyun@360.cn
智汇云官网:智汇云-企业数智化核心引擎(请用力戳)