竞态问题与RxJs

竞态问题通常指的是在多线程的编程中,输入了相同的条件,但是会输出不确定的结果的情况。虽然Js是单线程语言,但由于引入了异步编程,所以也会存在竞态的问题,而使用RxJs通常就可以解决这个问题,其使得编写异步或基于回调的代码更容易。

竞态问题

前边提到了竞态问题通常指的是在多线程的编程中,输入了相同的条件,但是会输出不确定的结果的情况。发生这种情况的主要原因是,当多个线程都对一个共享变量有读取-修改的操作时,在某个线程读取共享变量之后,进行相关操作的时候,别的线程把这个变量给改了,从而导致结果出现了错误。在这里的多个线程中,起码有一个线程有更新操作,如果所有的线程都是读操作,那么就不存在什么竞态条件。总体来说,最低是需要thread1#load - thread2#update这种的模式,当其中一个线程进行更新共享变量操作的时候,另一个线程不管是读取变量还是更新变量都容易出现错误,要么读取脏数据,要么丢失更新结果,通常会使用加锁或者原子操作的方式来消除竞态的影响。

回到Js当中,虽然Js是单线程语言,但由于引入了异步编程,所以也会存在竞态的问题。举一个简单的例子,我们经常会发起网络请求,假如我们此时需要发起网络请求展示数据,输入A时弹出B,输入B时弹出C,要注意反悔的数据都是需要通过网络发起请求来得到的,假设此时我们快速的输入了A又快速输入了B,如果网络完全没有波动的情况下,我们就可以正常按照顺序得到BC的弹窗,但是如果网络波动了呢,假设由于返回B的数据包正常在路上阻塞了,而C先返回来了,那么最后得到的执行顺序可能就是CB的弹窗了。在这里只是一个顺序问题,如果我们做搜索的时候,更加希望的是展示输入的最后的值的搜索结果,那么按照上边的例,我们希望得到最后输入的那个字母的下一个字母,也就是顺序输入AB希望得到C,但是却也有可能得到B

const fetch = text => {
if(!text) return Promise.resolve("");
const response = String.fromCharCode(text[text.length - 1].charCodeAt(0) + 1);
return new Promise(resolve => {
setTimeout(resolve, Math.random() * 1000, response);
})
} // 模拟快速输入`A B`
// 输出时而 `B C` 时而 `C B`
// 如果不是打印而是将值写到页面上 那么页面显示就出现错误了
fetch("A").then(console.log);
fetch("AB").then(console.log);

通常来说,对于这类需求,我们会在输入的时候加一个防抖函数,这样的话第一个输入就会被抹掉,这样在这里就不会造成快速输入的竞态问题了,这是属于从降低频率入手,尽量确保请求的有序。为什么说尽量呢,因为如果用户中间停顿了300ms也就是下边设置的值之后,再进行输入的话,依旧无法确保解决网络的原因造成的竞态问题,如果你把这个延时设置的非常大的话,那么就会造成用户最少等待n ms才能响应,用户体验并不好。

const fetch = text => {
if(!text) return Promise.resolve("");
const response = String.fromCharCode(text[text.length - 1].charCodeAt(0) + 1);
return new Promise(resolve => {
setTimeout(resolve, Math.random() * 1000, response);
})
} const d = function(time, fn){
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = null;
timer = setTimeout(() => fn(...args), time);
}
} const request = param => {
fetch(param).then(console.log);
}
const debouncedRequest = d(300, request);
debouncedRequest("A");
debouncedRequest("AB");

那么还有什么办法呢,或许我们也可以从确保顺序入手,请求携带一个标识,请求返回后根据标识判断是否渲染,这样的话就需要改动一下我们的fetch,把请求的参数也一并带上返回。这样看起来是完全解决了竞态的问题,但是似乎看起来并不是非常的漂亮,追求完美的同学可能会眉头一皱,觉得事情并不简单,这一段代码的执行结果依赖两个异步逻辑的彼此的执行顺序,而需要我们编写其他的代码去控制这个执行顺序,这个问题通常称作竞态危害。

const fetch = param => {
if(!param) return Promise.resolve({param, response: ""});
const response = String.fromCharCode(param[param.length - 1].charCodeAt(0) + 1);
return new Promise(resolve => {
setTimeout(resolve, Math.random() * 1000, {param, response});
})
} let tag = "";
const request = param => {
tag = param;
fetch(param).then((res) => {
if(res.param === tag) console.log(res.response);
});
}
request("A");
request("AB");

当然还有很多其他的方案可以处理这个问题,例如输入节流输入后开始请求的时候加一个全局的loading遮罩层,来阻止服务响应之前用户继续输入,或者在进行第二次请求的时候,取消前一次的请求,类似于useEffect返回的函数,取消上次的副作用。

对于请求取消的这个问题,并不是真的服务端收不到数据包了,只是说浏览器不处理这次请求的响应了,或者干脆我们自己直接本地不处理服务端的响应了,其实也很好理解,大部分情况下网络波动实际上是比较小的,当发起请求的时候数据包已经出去了,当你进行取消操作的时候,假如我们的取消操作是发出去了一个包用来告诉服务器取消前一个请求,这个取消数据包大部分情况下是不能追上之前发出去的请求数据包的,等这个数据包到的时候服务器都可能已经处理完了,所以实际上如果采用这个操作的话基本是个无效操作,由此现在的请求取消只是说浏览器取消了对于这个请求的响应处理而已,并不是服务器真的收不到数据了。

RxJs

RxJsReactive Extensions for JavaScript的缩写,起源于Reactive Extensions,是一个基于可观测数据流Stream结合观察者模式和迭代器模式的一种异步编程的应用库,RxJsReactive ExtensionsJavaScript上的实现。其通过使用Observable序列来编写异步和基于事件的程序,提供了一个核心类型Observable,附属类型ObserverSchedulersSubjects和受[Array#extras]启发的操作符mapfilterreduceevery等等,这些数组操作符可以把异步事件作为集合来处理。RxJs有中文文档https://cn.rx.js.org/manual/overview.html,可以定义函数在https://rxviz.com/中看到可视化的效果。

RxJs中用来解决异步事件管理的的基本概念是:

  • Observable: 可观察对象,表示一个概念,这个概念是一个可调用的未来值或事件的集合。
  • Observer: 观察者,一个回调函数的集合,它知道如何去监听由Observable提供的值。
  • Subscription: 订阅,表示Observable的执行,主要用于取消Observable的执行。
  • Operators: 操作符,采用函数式编程风格的纯函数pure function,使用像mapfilterconcatflatMap等这样的操作符来处理集合。
  • Subject: 主体,相当于EventEmitter,并且是将值或事件多路推送给多个Observer的唯一方式。
  • Schedulers: 调度器,用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如setTimeoutrequestAnimationFrame或其他。

RxJs上手还是比较费劲的,最直接的感受还是: 一看文章天花乱坠,一写代码啥也不会。在这里也仅仅是使用RxJs来处理上边我们提出的问题,要是想深入使用的话可以先看看文档。

那么我们就用RxJs来解决一下最初的那个问题,可以看到代码非常简洁,在这里我们取了个巧,直接将Observable.createobserver暴露了出来,实际上因为是事件触发的,通常都会使用Observable.fromEvent来绑定事件,在这里演示我们是需要自己触发的事件了,也就是runner.next,这里最重要的一点就是借助了switchMap,他帮助我们管理了在流上的顺序,取消了上次回调的执行。在下边这个示例中,可以看到其只输出了C,达到了我们想要的效果。

// 这块代码可以在`https://cn.rx.js.org/`的控制台直接运行
const fetch = text => {
if(!text) return Promise.resolve("");
const response = String.fromCharCode(text[text.length - 1].charCodeAt(0) + 1);
return new Promise(resolve => {
setTimeout(resolve, Math.random() * 1000, response);
})
} let runner;
const observable = Rx.Observable.create(observer => runner = observer);
observable
// .debounceTime(300) // 可以加入防抖
.switchMap(param => fetch(param))
.subscribe(console.log); runner.next("A");
runner.next("AB");

每日一题

https://github.com/WindrunnerMax/EveryDay/

参考

https://cn.rx.js.org/
https://zhuanlan.zhihu.com/p/104024245
https://www.zhihu.com/question/324275662
https://juejin.cn/post/6910943445569765384
https://juejin.cn/post/6844904051046350862
https://juejin.cn/post/7098287689618685966
https://juejin.cn/post/6970710521104302110

竞态问题与RxJs的更多相关文章

  1. linux设备驱动归纳总结(四):5.多处理器下的竞态和并发【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-67673.html linux设备驱动归纳总结(四):5.多处理器下的竞态和并发 xxxxxxxxxx ...

  2. linux设备驱动归纳总结(四):4.单处理器下的竞态和并发【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-67005.html linux设备驱动归纳总结(四):4.单处理器下的竞态和并发 xxxxxxxxxx ...

  3. Smart210---学习记录 竞态与并发

    竞态与并发 自旋锁 若一个进程要访问临界资源,测试锁空闲,则进程获得这个锁并继续执行:若测试结果表明锁扔被 占用,进程将在一个小的循环内重复“测试并设置”操作,进行所谓的“自旋”,等待自旋锁持有者释 ...

  4. Linux驱动设计——并发与竞态控制

    并发的概念:多个执行单元同时.并行被执行. 共享资源:硬件资源(IO/外设等),软件上的全局变量.静态变量等. 四种并发控制机制(对共享资源互斥的访问):原子操作.自旋锁(spinlock).信号量( ...

  5. 《Linux 设备驱动程序》读后感。 并发,竞态,死锁。

    1. 概念 并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行. 来源: 1. Linux ...

  6. Linux内核分析(七)----并发与竞态

    原文:Linux内核分析(七)----并发与竞态 Linux内核分析(七) 这两天家里的事好多,我们今天继续接着上一次的内容学习,上次我们完善了字符设备控制方法,并深入分析了系统调用的实质,今天我们主 ...

  7. Go 初体验 - 并发与锁.3 - 竞态

    竞态,就是多个协程同时访问临界区,由并发而产生的数据不同步的状态. 这个说的有点low,没办法,我就是这么表达的,官方的请度娘. 先上代码: 输出: 为何不是1000?就是因为竞态,发生竞态后,最终的 ...

  8. 漫画|Linux 并发、竞态、互斥锁、自旋锁、信号量都是什么鬼?(转)

    知乎链接:https://zhuanlan.zhihu.com/p/57354304 1. 锁的由来? 学习linux的时候,肯定会遇到各种和锁相关的知识,有时候自己学好了一点,感觉半桶水的自己已经可 ...

  9. UNIX高级环境编程(10)进程控制(Process Control)- 竞态条件,exec函数,解释器文件和system函数

    本篇主要介绍一下几个内容: 竞态条件(race condition) exec系函数 解释器文件    1 竞态条件(Race Condition) 竞态条件:当多个进程共同操作一个数据,并且结果依赖 ...

  10. iOS 10 的一个重要更新-线程竞态检测工具 Thread Sanitizer

    本文介绍了 Xcode 8 的新出的多线程调试工具 Thread Sanitizer,可以在 app 运行时发现线程竞态. 想想一下,你的 app 已经近乎大功告成:它经过精良的打磨,单元测试全覆盖. ...

随机推荐

  1. 你和时间管理大师,就差一个开源工具「GitHub 热点速览」

    在这个快节奏的生活中,我们努力地在平衡工作.生活和个人发展,但常常感到时间不够用.如何在繁忙的日程中找到一丝丝"喘息"的机会,这个名叫 cal.com 开源项目能让你更轻松地管理日 ...

  2. 【SHELL】查找包含指定字符串的目录、在找出的路径中找出指定格式的文件、并统计出数量

    查找包含字符串"skull"的目录.在找出的路径中找出格式".c/.cpp/.h"的文件.并统计出数量 find . -path ./out -prune -o ...

  3. [转帖]如何监控Redis性能指标(译)

    Redis给人的印象是简单.很快,但是不代表它不需要关注它的性能指标,此文简单地介绍了一部分Redis性能指标.翻译过程中加入了自己延伸的一些疑问信息,仍然还有一些东西没有完全弄明白.原文中Metri ...

  4. [转帖]Java程序在K8S容器部署CPU和Memory资源限制相关设置

    2019-04-297279 版权 本文涉及的产品 容器服务 Serverless 版 ACK Serverless,317元额度 多规格 推荐场景: 立即试用 容器镜像服务 ACR,镜像仓库100个 ...

  5. [转帖]【Python】计算程序运行时间的方法总结

    一.第一种方法 利用time包: import time def test(): start_time = time.time() # 记录程序开始运行时间 s = 0 for i in range( ...

  6. gcore的学习

    gcore的学习-解决jmap无法生成dump文件的一种方法 背景 周末在跆拳道馆看孩子练跆拳道. 开着笔记本翻到了 扣钉日记 公众号里面的讲解 想着自己也遇到过无法保存dump文件的情况. 所以想学 ...

  7. Oracle TNS 异常问题处理

    今天下午快下班时同事找我说自己的性能测试Oracle数据库 连不上了. 然后自己连上去简单看了一下. 因为已经是事后了, 所以没有截图,只通过文字说明. 环境说明:Win2012r2 + Oracle ...

  8. IPMI的简单使用

    背景 公司一台十一年前的服务器砸到我手中,要重装CentOS7的操作系统. 本着不想进机房, 不想格式化U盘的想法, 想用BMC进行安装系统. 遇到的第一个问题是不知道密码. 询问之前的机器持有人,也 ...

  9. 【译文】IEEE白皮书 6G 太赫兹技术的基本原理 2023版

    第一章 简介 太赫兹波是介于微波和光波之间的光谱区域,频率从 0.1THz ~ 10THz 之间,波长在 3mm ~ 30μm 之间.提供大块连续的频带范围以满足对 Tbit/s 内极高数据传输速率的 ...

  10. vim 从嫌弃到依赖(3)——vim 普通模式

    在上一篇中,我们提到vim的几种模式,并且给出了一些基本的操作命令,包括移动光标,删除.替换操作.并且给出了几个重要的公式,理解这个公式对于理解vim和提高使用vim的效率来说至关重要.所以在这篇文章 ...