learn-tech/专栏/JavaScript进阶实战课/16为什么环形队列适合做Node数据流缓存?.md
2024-10-16 06:37:41 +08:00

11 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        16 为什么环形队列适合做Node数据流缓存
                        你好,我是石川。

前面几讲讲完了栈这种数据结构我们再来看看队列queue。队列对于你来说可能不算是一种陌生的数据结构和栈相反列队通常遵循的是先进先出FIFOFirst In, First Out的原则。你可以把它想象成在咖啡厅买咖啡时要排的队基本是先到先得最后来的最后买到不能插队。如果你强行插队的话那每个人的排队时长都会打乱。

只是实现一个队列并不复杂重要的是你要理解队列在编程中的应用。既然我们在讲JS就举几个身边的例子比如我们的浏览器就是通过引擎来做线程和任务的管理。在使用Node的应用中环形队列可以用来做数据流的缓存。这一讲首先我们快速地了解下队列的核心然后通过它在JS引擎了解它们的使用场景最后我们通过学习一种特殊的环形队列来解答开篇的问题就是为什么环形队列适合用来缓存数据流。

如何实现队列和双队列

首先我们来看看如何实现一个简单的队列。数据结构中队列的核心思想和我们排队买票看电影一样关键是谁排在前面谁就可以先买到票。和排队一样在数据结构中我们可以有入队、出队的概念。入队enqeue顾名思义就是在队伍后面加了一个人排队。这里的实现和我们看的栈里面的入栈是类似的所以也可以用push来完成。下面我们再来说说出队按照先入先出的规则排在最前面的人买完票了以后就会出队dequeue。它的实现可以通过JavaScript中自带的shift通过shift我们可以去掉一个数组里面最开头的一个元素。

除了队列还有一个概念是双队列deque。虽然说通常我们排队的时候都是遵循先进先出的规则但是在有些特殊的情况下也会有特例。比如大家在排队等车看到一位女士带着很小的宝宝在大热天高温下等待如果一队的人同意的话大家一般会让她们排到前面。那么在JavaScript中呢同样有一个unshift()的方法可以用来做到插队我们在下面例子里把它叫做dequeAdd。还有另外一种情况就是如果有的人在队尾等不及了也有可能离开这样的话我们可以借用弹出栈的pop()来实现在下面例子中我们可以管它叫dequeRemove吧。

在JavaScript里面我们可以通过下面的方式来实现一个队列。

class Queue { constructor () { this.queue = []; } enqueue(item) { return this.queue.push(item); } dequeue() { return this.queue.shift(); } dequeAdd(item) { return this.queue.unshift(item); } dequeRemove(item) { return this.queue.pop(item); } peek() { return console.log(this.queue[0]); } }

通过队列看浏览器任务管理

下面我们再来看看Chrome浏览器是如何通过队列来实现线程管理的。首先我们来了解下进程process和线程thread分别是什么之间是什么关系。我们拿Chromium来举例Chromium用的是多进程的架构multi-process architecture。我们在浏览器中每当打开一个新页面的时候就是一个新的浏览器进程。

在一个浏览器进程里会有多个线程。Chromium中有两个核心线程一个是主线程另外一个是IO线程。因为浏览器是面向前端用户的所以对于它的架构设计来说最主要的目标是让主线程和IO线程可以快速响应。为了达到这个目的就要把阻塞IO或复杂运算分给其它的线程去处理线程间通信问题通过消息传递来解决。

所以可以说Chromium用的是一个高并发但不算是高并行的架构。对于页面加载的脚本中要执行的任务会采用任务队列的方式通过事件循环给到UI主线程。如果问题是主线程可以解决的就会处理如果处理不了的就会给到IO线程或特殊线程来处理处理的结果会通过消息通信的方式给到主线程。

在浏览器进程中除了主线程、IO线程和特殊线程外还有一个用来处理通用任务的线程池。线程池有一个共享的任务队列。我们知道在处理高并发时人们通常通过锁结构来确保线程安全而在Chromium线程管理当中并不提倡用锁的结构而是提倡序列或虚拟线程管理的概念。因为在Google看来序列本身就带有线程安全性。这是怎么做到的呢因为在虚拟线程管理中只有当一个任务执行完下一个任务才有可能被分配到线程池中的另一个工作线程来执行所以下一个任务肯定是基于上一个任务的结果来执行的。所以虽然对于不同的工作线程来讲它们之间的工作是并行的但是对于一组需要串行处理的任务来说它们的执行是有先后顺序的。

这里我们可以打一个比方,比如我们在项目管理中,会有一个需求池,这个需求池就是我们的任务队列,而虚拟线程管理就如同一个项目经理。

项目经理的工作是根据任务和团队来制定项目计划。比如根据需求执行中有两个任务分别是设计和开发这两个任务需要按顺序来开发团队说你的设计UI没出来我是不会开始写代码的这时作为项目经理你会把这两个任务作为串行任务也就是任务2的开始基于任务1的结束。虽然这解决了线程安全问题但是同时项目经理也清楚投入到项目的生产力是有限的怎么能通过生产关系的优化让资源更有效的被利用起来呢

这时我们可以用迭代的方式。设计团队做迭代1中的设计然后确保完成再交给开发团队来开发。这时当开发团队在做迭代1的开发时设计团队已经在继续做迭代2的设计了。对于迭代1中设计和开发这两个任务来说它们是按照序列串行的。同时对于两个团队来讲他俩的工作是并行的。这样既降低了项目风险又可以做到一定程度的并发。

那么这样是说Chromium就完全不支持锁了吗也不是。那什么时候可以用锁结构呢同样举个例子这就好比产品和研发两个员工worker thread产品宣讲完业务逻辑研发在开发过程中发现当时讨论的流程图有个问题两个人需要同时修改一份流程文档这个时候为了避免内容被相互覆盖两个人商量好了应该只有一个人去改。所以在Chromium中通常当有多个线程worker thread可以访问一个数据资源但是同一时间只允许一个线程来访问这些资源或数据的时候会用到锁。在Chromium当中使用的是互斥锁mutex

环形队列和线程间数据传递

说完了常见的队列我们再来看在一种特殊的队列就是环形队列。一个环形队列是首尾相连的。它也叫做环形缓冲区ring buffer可以用于进程或线程之间的数据传递。

回答开篇的问题为什么环形队列适合做Node数据流缓存你可能会问这样的队列为什么会有用那是因为它最核心的好处是当一个数据元素被用掉后其余数据元素不需要移动其存储位置。我们来看看它是怎么做到这一点的。对于一个环形队列来说我们一般需要两个指针一个是头指针一个是尾指针。头指针指向的是下一个要加入的数据尾指针指向下一个要读取的数据。当数据被读取时我们就增加尾指针当数据被写入的时候我们就增加头指针。

举个例子假设我们有一个16位的缓冲第一步我们加入了4位数据头指针就移动到了3。如果再加3个的话头指针就移动到了6。如果这时我们读取了前4个那么尾指针就会到4。

用一个形象的比喻,大家如果在美国的一些餐厅吃过饭,可能会见过一个点餐轮盘。来餐厅的顾客一般会把想吃的东西写在纸上,然后放到轮盘上,厨师会按照顺序从轮盘上把点餐的菜单拿下来,然后来做饭。这就很像是一个环形队列。

那么在程序中这种环形队列如何实现呢从实现的角度一般会建立两个数组一个是原数组用来定义环形队列的属性和方法第二个是实际用来存放数据的环形队列数组。在原数组里面主要存放3个关键属性分别是头指针、尾指针和环形队列长度。同时包含几个核心方法原数组中属性的获取和设置以及环形队列数组中数据的读和写。通过这种方式就可以实现一个环形队列了。

下面我们可以来看看它在缓存数据流中的应用。这种环形队列通常会和“生产者消费者”模式一起用也通常会加一个互斥锁到环形队列的读、写方法里用来实现互斥访问。如下图所示我们可以看到有4个工作线程。2个是生产者2个是消费者。 生产者负责写入数据,消费者读取数据。通过加锁的方式,在同一时间,只有一个生产者可以写入,在读的时候,也只有一个消费者可以读取。

在数据流这种大量数据持续进入到列队中,再从队列中取出做缓存处理的情况下,使用环形队列就大大增加了生产者和消费者之间协同合作的效率。

总结

在这一讲当中我们通过对队列的原理介绍学习了它在浏览器线程管理中的应用之后通过对环形队列的的原理介绍学习了它在缓存数据流中的应用。数据流缓冲在很多应用中都有体现除了进程管理外在网络和文件系统中都会用到数据流缓冲。在网络中字节数据都是按照包的大小分成块儿来传输的在文件系统中数据块儿都是根据内核的缓冲大小分成块儿来处理的。所以无论是HTTP的数据请求和反馈还是文件到目的地的传输这些数据的传输都会用到环形队列缓冲。

思考题

在我们用互斥锁的时候,会发现它有一个劣势,就是共享资源,也就是环形队列每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程,这在某种意义上是一个悲观锁。除此之外,还有另外的一种方式是原子操作,它是针对某个值的单个互斥操作。你知道如何通过原子操作来实现环形队列缓冲吗?

期待在留言区看到你的分享,我们一起交流讨论。另外,也欢迎你把今天的内容分享给更多的朋友。我们下期再见!