Web开发编程网
分享Web开发相关技术

关于限制并发请求的一道面试题

事情是这样,上周四给团队专栏发了一篇文章,发完之后忘了把账号切回自己的账号。然后下午搬砖中场休息的时候刷掘金,在首页看到一篇关于限制并发请求的文章,觉得有意思就点开看了。我扫了一眼文章中的题目,没有看文章内容,然后就开始自己解题了。尴尬的是我审错题了,一开始用团队账号在评论里写了个 naive 的答案。然而更尴尬的是在我意识到自己审错题之后,还写不出正确答案……

题目是这样的:

req

忽略我一开始那个 naive 的答案,在我准备正确地解这个题的时候,想的是写一个通用方法,限制异步请求能执行的次数。那个通用函数写出来了,长这样:

const limitConcurrency = (fn, max) => {
  const pendingTasks = new Set();
  return async function(...args) {
    while (pendingTasks.size >= max) {
      await Promise.race(pendingTasks);
    }

    const promise = fn.apply(this, args);
    const res = promise.catch(() => {});
    pendingTasks.add(res);
    await res;
    pendingTasks.delete(res);
    return promise;
  };
};
复制代码

这个 limitConcurrency 函数在闭包里记录了当前还没 resolve 的 promise,然后在 while 循环里判断当前进行中的异步操作是否达到了设定的上限;如果达到了就用 Promise.race 等最快的那个异步执行完;当并发的异步操作数量没有达到上限时,继续执行当前的异步操作,并将当前的异步操作加到 pendingTasks 里面,在当前异步操作 resolve 的时候再将其从 pendingTask 里面删掉。

然后再回到题目,一开始我思维比较局限,想着我必须要等最后一个异步请求执行完再执行回调。怎么判断所有请求都执行完了呢?我又用了一个 naive 的方法,当 urls 数组里面最后一个请求执行完了,我就当所有请求执行完了。我最后这样写的:

function sendRequest(urls, max, callback) {
    const limitFetch = limitConcurrency(fetch, max);
    
    async function go(urlList){
        const [head, ...tail] = urlList;
        if(tail.length === 0) {
            await limitFetch(head);
            return callback();
        }
        limitFetch(head);
        go(tail);
    }
    go(urls);
}
复制代码

动脑子想一想也知道最后一个请求不一定是最后执行完。但是我卡在这里了,没办法了,然后我发沸点求助了,然后各路英雄各显神通,看到他们的答案我感到怀疑人生,这么简单的问题我怎么就卡壳了?

精彩答案有很多,这里我只挑出我觉得最精彩的答案,来自幻☆精灵

function sendRequest(urls, max, callback) {
  const len = urls.length;
  let idx = 0;
  let counter = 0;

  function _request() {
    // 有请求,有通道
    while (idx < len && max > 0) {
      max--; // 占用通道
      fetch(urls[idx++]).finally(() => {
        max++; // 释放通道
        counter++;
        if (counter === len) {
          return callback();
        } else {
          _request();
        }
      });
    }
  }
  _request();
}
复制代码

这个答案如此精巧简洁,我今天上午看到这个答案,然后出去爬山,整个途中都在回味这段代码的精妙…… 它用最少的信息表达了异步和并发的本质。写出这种代码,需要的不仅仅是编程技巧,更多的是对异步和并发的理解。在我完整理解了这段代码之后,晚上回到家再来看我之前的答案,发现我离正确答案就只差一丢丢了!

最终完整答案如下:

const limitConcurrency = (fn, max) => {
  const pendingTasks = new Set();
  return async function(...args) {
    while (pendingTasks.size >= max) {
      await Promise.race(pendingTasks);
    }

    const promise = fn.apply(this, args);
    const res = promise.catch(() => {});
    pendingTasks.add(res);
    await res;
    pendingTasks.delete(res);
    return promise;
  };
};

async function sendRequest(urls, max, callback) {
  const limitFetch = limitConcurrency(fetch, max);
  await Promise.all(urls.map(limitFetch));
  callback();
}
复制代码

limitConcurrency 已经保证了我的全部请求的并发上限,我只需要用 Promise.all 来处理每个并发频道的最后请求就行了……

从代码简洁性来看,当然是幻☆精灵前辈的解法更优,但我觉得我的解法也不无可取之处。我提供的答案最大的好处是,它抽象出了一个通用函数,这个通用函数能复用。

作者:serialcoder
链接:https://juejin.im/post/5c8e3401f265da67da2b0acf
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

未经允许不得转载:WEB开发编程网 » 关于限制并发请求的一道面试题

WEB开发编程网

谢谢支持,我们一直在努力

安全提示:您正在对WEB开发编程网进行赞赏操作,一但支付,不可返还。