竞态问题:深入理解与解决方案

简介: 竞态问题:深入理解与解决方案

竞态的定义

竞态(race condition)是指在多线程编程中,多个线程对同一数据进行读写操作时,最终的结果取决于各个线程的执行顺序

竞态问题

竞态问题是由于多个线程之间的相互影响而导致的,这种影响可能会导致程序出现不可预测的结果

在程序中,竞态问题通常会出现在共享资源的访问上,如共享内存、文件、网络连接等。为了避免竞态问题,我们可以使用锁、信号量、互斥量等同步机制来保证共享资源的访问顺序

这个概念最早应该出自后端,在前后端分离后,到现在前端应用框架等技术越来越成熟完善后,前端领域里面也出现了竞态问题

前端中的竞态问题

在前端开发中,竞态问题通常会出现在异步请求的场景中,如搜索、分页、选项卡切换场景、保存提交、下载等等场景

例如搜索场景中的静态问题,用户输入关键字后,前端会向后端发送异步请求获取数据并展示。如果用户在请求返回前再次输入关键字,那么前端会再次向后端发送异步请求。如果第二次请求返回的速度比第一次请求快,那么就会出现第二次请求的数据覆盖了第一次请求的数据的情况

小扩展

之前写过的一篇文章中遇到的问题也可以称为竞态问题,这里面遇到不是单一性的问题,有各种复杂业务场景以及某些骚操作写法融合在在一起,因此不能通过单一对请求的拦截等处理全部解决项目中的问题,选择了从页面添加 loadind 层的方式解决的实际问题,有兴趣的可以看下

项目提交按钮没防抖,差点影响了验收

如何避免竞态问题

从请求方面来看,有两种方式,分别是 取消(终止)忽略 请求

取消请求

fetch , axios 中可以使用 AbortController 接口的 abort()  方法

fetch

下面是 vue2 中的 fetch 中使用 abort() 终止方法

mounted() {
  this.controller = new AbortController();
  this.signal = this.controller.signal;
},
methods: {
    download() {
      const url = "https://mdn.github.io/dom-examples/abort-api/sintel.mp4";
      fetch(url, { signal: this.signal })
        .then((response) => {
          console.log("Download complete", response);
        })
        .catch((err) => {
          console.error(`Download error: ${err.message}`);
        });
    },
    abort() {
      this.controller.abort();
      console.log("Download aborted");
    },
},

点击下载按钮下载视频,然后点击终止后,请求终止的效果

image.png

axios

axios 新版本终止的效果和 fetch 用法一样

const controller = new AbortController();
axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   ...
});
controller.abort()

XMLHttpRequest

XMLHttpRequest(XHR)对象可以使用 XMLHttpRequest.abort() 方法终止请求

var xhr = new XMLHttpRequest(),
  method = "GET",
  url = "https://xxx.xxx/api";
xhr.open(method, url, true);
xhr.send();
if (OH_NOES_WE_NEED_TO_CANCEL_RIGHT_NOW_OR_ELSE) {
  xhr.abort();
}

忽略请求

通过封装 promise 请求的方式,判断请求是否结束,如果结束正常发送请求,如果没有结束则忽略当前请求

如下图示例,如果相同请求未结束提示请求正在进行中

image.png

请求封装

通过 pendingRequests map 变量来判断请求是否结束,如果变量为空,则添加到 map 对象中,否则就判断当前请求是否存在,如果存在则忽略当前请求,如果不存在,则添加当前请求到 map 对象中

pendingRequests: new Map()
createRequestManager() {
    const self = this;
    function sendRequest(data) {
      console.log('data: ', data)
      console.log('pendingRequests: ', self.pendingRequests)
      if (!self.pendingRequests.has(data)) {
        const promise = self.makeAsyncRequest(data);
        promise
          .then((response) => {
            console.log('promise then')
            self.pendingRequests.delete(data);
          })
          .catch((error) => {
            console.log('promise catch')
            console.error(error);
            self.pendingRequests.delete(data);
          });
        self.pendingRequests.set(data, promise);
        console.log('self.pendingRequests', self.pendingRequests)
      } else {
        console.log(`Request for data: ${data} is already pending.`);
      }
    }
    return {
      sendRequest,
    };
  },

调用函数

封装函数中的调用函数,这里使用 setTimeout 模拟实际请求

makeAsyncRequest(data) {
  return new Promise((resolve, reject) => {
    // 模拟异步请求,这里可以是实际的网络请求
    setTimeout(() => {
      resolve(`Response for data: ${data}`);
    }, 2000);
  });
},

异步请求(添加忽略) 按钮点击事件

download2() {
  const requestManager = this.createRequestManager();
  requestManager.sendRequest("Data 1");
},

防抖,节流

可以使用 lodash 库的 debouncethrottle 方法实现防抖和节流,也可以自定义函数实现类似功能,vue 可以使用全局注册的指令来实现防抖节流

Vue2 添加节流

main.js 添加全局指令

// 定义指令
Vue.directive('throttle', {
  bind: (el, binding) => {
    let throttleTime = binding.value // 防抖时间
    if (!throttleTime) {
      // 用户若不设置节流时间,则默认2s
      throttleTime = 2000
    }
    let cbFun
    el.addEventListener(
      'click',
      (event) => {
        if (!cbFun) {
          cbFun = setTimeout(() => {
            cbFun = null
          }, throttleTime)
        } else {
          event && event.stopImmediatePropagation()
        }
      },
      true
    )
  }
})

在按钮中使用,按需设置 v-throttle 参数即可

<div
  @click="save"
  v-throttle
>
  <span style="">保存</span>
</div>

示例代码地址

github.com/gywgithub/F…

总结

关于前端方面的竞态的问题,请求终止都是使用 abort() 方法进行终止;忽略请求方面是使用了 map 对象进行记录判断请求是否存在的等方案;还可以使用 防抖节流 等方案处理问题;在实际业务场景中需要根据真实的业务场景合理考虑技术方案,在复杂的业务中可能也需要结合 loading 等UI层的效果来提升用户的体验

目录
相关文章
|
7月前
|
存储 前端开发 JavaScript
如何解决前端常见的竞态问题?
如何解决前端常见的竞态问题?
135 0
关于死锁的原因及解决方案
关于死锁的原因及解决方案
198 0
|
2月前
|
安全 Java 开发者
在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制
【10月更文挑战第3天】在多线程编程中,确保数据一致性与防止竞态条件至关重要。Java提供了多种线程同步机制,如`synchronized`关键字、`Lock`接口及其实现类(如`ReentrantLock`),还有原子变量(如`AtomicInteger`)。这些工具可以帮助开发者避免数据不一致、死锁和活锁等问题。通过合理选择和使用这些机制,可以有效管理并发,确保程序稳定运行。例如,`synchronized`可确保同一时间只有一个线程访问共享资源;`Lock`提供更灵活的锁定方式;原子变量则利用硬件指令实现无锁操作。
32 2
|
5月前
|
安全 云计算
云计算自旋锁问题之在LogFileProfiler::AddProfilingData函数中使用锁如何解决
云计算自旋锁问题之在LogFileProfiler::AddProfilingData函数中使用锁如何解决
49 3
|
6月前
|
Java
探秘死锁:原理、发生条件及解决方案
探秘死锁:原理、发生条件及解决方案
154 1
|
5月前
|
安全 Java 开发者
Java并发编程中的线程安全问题及解决方案探讨
在Java编程中,特别是在并发编程领域,线程安全问题是开发过程中常见且关键的挑战。本文将深入探讨Java中的线程安全性,分析常见的线程安全问题,并介绍相应的解决方案,帮助开发者更好地理解和应对并发环境下的挑战。【7月更文挑战第3天】
109 0
|
6月前
|
并行计算 安全 Java
多线程编程中的线程安全问题与解决方案*
多线程编程中的线程安全问题与解决方案*
|
7月前
|
算法 Java
Java多线程基础-13:一文阐明死锁的成因及解决方案
死锁是指多个线程相互等待对方释放资源而造成的一种僵局,导致程序无法正常结束。发生死锁需满足四个条件:互斥、请求与保持、不可抢占和循环等待。避免死锁的方法包括设定加锁顺序、使用银行家算法、设置超时机制、检测与恢复死锁以及减少共享资源。面试中可能会问及死锁的概念、避免策略以及实际经验。
124 1
|
7月前
|
传感器 安全 程序员
【C++多线程 同步机制】:探索 从互斥锁到C++20 同步机制的进化与应用
【C++多线程 同步机制】:探索 从互斥锁到C++20 同步机制的进化与应用
510 1