实习的收获(3)--JS中的竞态问题

67 阅读4分钟

应该是最后一篇了,因为种种,作者决定下一段实习去北京

💼 需求背景

​场景​​:需要判断一个文件的存在情况,根据文件存在情况判断后续操作.

一种错觉还是现在的我还不能明悟,总感觉大家在js中习惯使用异步方法(也包括我自己), 平平无奇的某一天班,同事处理文件操作需求的时候,并没有达到理想状况, 检查之后才发现,原来是有一个异步方法的处理出现了错误---发生了竞态

竞态条件指的是:程序的输出或行为依赖于事件或线程无法控制的时序。

小明曾经在MDN中,看到过网络请求场景中的竞态问题(HTTP 条件请求 - HTTP | MDN)

image.png

在当初的工作需求场景中,它是这样发生的:

  1. 时刻 T1: 你的代码使用 fs.exists()或 fs.access()检查文件 /path/to/file是否存在。
  2. 时刻 T2: 检查返回 true,文件存在。你的程序决定进行下一步操作(比如读取文件)。
  3. 时刻 T3: 在你调用 fs.readFile()之前的极短瞬间,由于某些原因(例如另一个进程、另一个异步操作),这个文件被删除修改了。
  4. 时刻 T4: 你的 fs.readFile()开始执行,但它会立即失败,因为它在 T3 时刻所基于的状态(文件存在)已经失效了。

关键问题在于exists/access的检查结果只是一个  “快照” ,它只在检查的那个瞬间是准确的。在异步的“检查”和“操作”之间,文件系统的状态可能已经改变。

错误的模式:检查然后操作(Check-then-Act)

简单用代码举个例子,

const fs = require('fs');
const path = '/path/to/file';

// ❌ 错误的做法:存在竞态条件
fs.access(path, fs.constants.F_OK, (err) => {
  if (err) {
    console.log('文件不存在');
    return;
  }

  // 在这个时间点,文件可能被删除了!
  fs.readFile(path, 'utf8', (err, data) => {
    if (err) {
      // 这里可能会意外出错,例如 ENOENT (文件不存在)
      console.error('读取文件出错:', err);
      return;
    }
    console.log(data);
  });
});

使用 fs.promises的 async/await语法,这个错误看起来更“自然”(上面说的场景,当时也是这种方式),但本质一样:

import fs from 'fs/promises';

// ❌ 同样是错误的做法:依然存在竞态条件
async function readFileSafely(filePath) {
  try {
    await fs.access(filePath); // 检查
    // 检查后,操作前,文件可能被删除
    const data = await fs.readFile(filePath, 'utf8'); // 操作
    return data;
  } catch (err) {
    console.error('操作失败:', err);
  }
}

没什么好说的,大家尽可能避免这种问题就好,推荐下面这种

正确的解决方案:直接操作,处理错误(Just Do It, Handle Errors)

1. 回调函数风格的正确做法

const fs = require('fs');

// ✅ 正确的做法:直接操作,处理错误
fs.readFile('/path/to/file', 'utf8', (err, data) => {
  if (err) {
    // 在这里统一处理所有可能的错误
    if (err.code === 'ENOENT') {
      console.log('文件不存在');
    } else if (err.code === 'EACCES') {
      console.log('权限不足');
    } else {
      console.error('其他错误:', err);
    }
    return;
  }
  // 成功读取数据
  console.log(data);
});

2. Promise/async-await 风格的正确做法

import fs from 'fs/promises';

// ✅ 正确的做法:直接操作,处理错误
async function readFileSafely(filePath) {
  try {
    const data = await fs.readFile(filePath, 'utf8'); // 直接操作
    return data;
  } catch (err) {
    // 在这里统一处理所有可能的错误
    if (err.code === 'ENOENT') {
      console.log('文件不存在');
    } else if (err.code === 'EACCES') {
      console.log('权限不足');
    } else {
      console.error('其他错误:', err);
    }
    // 可以选择重新抛出错误,或者返回一个默认值
    // throw err;
    return null;
  }
}

又叫小明学到了,

后面也为此去看了很多大佬博客,忘记具体是哪篇文章了,当中提到:

Node.js 文件系统操作的设计哲学就是:直接进行你最终想要的操作,如果出错,再通过错误对象来处理具体的原因。 这是因为文件操作本身(如 fs.readFilefs.writeFile)在内部已经包含了必要的检查。你应该相信这些 API,并让它们给你一个统一的错误处理入口.

站在现在的时间点,回望我3个月的实习期,其中还有一些需求场景用到了Tampermonkey(篡改猴/油猴)插件, 当初用ai直接产出,尽在意最终的需求实现,以后准备单独写个专栏,记录学习Tampermonkey.

小明就要开启下一段故事了(实习),

“虽然要开启新旅程啦,但会一直记得在老东家和各位一起奋斗的日子!祝mt(佩奇)和同事们吃好喝好没烦恼,业绩健康都满分,继续保持联系哦~”