在异步函数中运行并发HTTP请求

时间:2018-06-23 02:52:21

标签: javascript asynchronous

我正在一个需要异步功能的项目中,该功能大致等效于以下内容

async function task(url) {

    var r1 = await fetch(url).then(resp => resp.text());

    var r2 = await fetch(url + "/" + r1).then(resp => resp.json());

    //r2 is an array of urls

    var total = 0;
    for (var u of r2) {
        tmp = await fetch(u).then(resp => resp.text());
        total += parseInt(tmp)
    }
    return total
}

问题在于r2中有数百个元素,每个元素都是一个URL。如果我按顺序执行此功能,则需要很长时间才能完成。我想同时运行10个URL(可以将其调整为其他数字),想知道如何重写异步函数。

4 个答案:

答案 0 :(得分:3)

将初始数组分成10个部分,然后等待每个块以Promise.all完成,然后开始下一个数组:

async function getTotal(urlPart, subArr) {
  const resps = await Promise.all(subArr.map(url =>
    fetch(url).then(resp => resp.json())
  ))
  return resps.reduce((a, b) => a + b);
}

async function task(url) {
  const r1 = await fetch(url).then(resp => resp.text());
  const r2 = await fetch(url + "/" + r1).then(resp => resp.json());

  const chunks = [];
  const { length } = r2
  for (let i = 0; i < length; i += 10) {
    chunks.push(r2.slice(i, i + 10));
  }
  let total = 0;
  for (const subArr of chunks) {
    total += await getTotal(urlPart, subarr);
  }
  return total;
}

答案 1 :(得分:2)

这是我几年前创建的一些代码,可让您创建“并行”队列

const makeQueue = length => {
    length = (isNaN(length) || length < 1) ? 1 : length;
    const q = Array.from({length}, () => Promise.resolve());
    let index = 0;
    const add = cb => {
        index = (index + 1) % length;
        return (q[index] = q[index].then(() => cb()));
    };
    return add;
};

这将允许最多10个同时请求(或您作为参数传递的任何内容)

在您的代码中,我想您可以使用它

async function task(url) {
    const q = makeQueue(10); // 10 requests at a time

    var r1 = await fetch(url).then(resp => resp.text());

    var r2 = await fetch(url + "/" + r1).then(resp => resp.json());

    return Promise.all(r2.map(u => q(() => fetch(u).then(resp => resp.text())))).then(v => v.map(parseInt).reduce((a, b) => a+b));
}

返回也可以

return Promise.all(r2.map(u => q(() => fetch(u).then(resp => resp.text()).then(parseInt)))).then(v => v.reduce((a, b) => a+b));

细分为

const fetch1 = u => fetch(u).then(resp => resp.text()).then(parseInt);

const promises = r2.map(u => q(() => fetch1(u)));

return Promise.all(promises).then(v => v.reduce((a, b) => a+b));

这种方法的好处是在最大的时间内应该有10个“行进中”请求

请注意,浏览器倾向于限制每个主机的并发请求数,因此,如果队列大小大于6(我认为这是最常见的限制),您可能看不到任何改善

答案 2 :(得分:1)

在这里欣赏所有好的答案!我研究了它们,并提出了以下解决方案,我认为这稍微简单了一些(对于我们许多初学者而言):-)

此解决方案一开始并未将所有的url提取作业分开,因为不确定每次url提取将花费多少时间。 相反,它使每个工作程序都经过所有URL,如果将一个URL分配给另一工作程序,它将继续前进到下一个。

var tasks
var total = 0
var gId = 0
var workerId
manager(4)
async function manager(numOfWorkers) {
    var workers = []
    tasks = r2.map(function(u) {return {id: gId++, assigned: -1, url: u }})
    for (var i=0; i<numOfWorkers; i++) { workers.push(worker()) }
    await Promise.all(workers)
    console.log(total)
}
async function worker() {
    var wid = workerId; workerId ++;
    var tmp;
    for (var u of tasks) {
        if (u.assigned == -1) {
            u.assigned = wid;
            console.log("unit " + u.id + " assigned to " + wid)
            tmp = await fetch(u.url).then(r=>r.text())
            total += parseInt(tmp);
        }
    }
}

答案 3 :(得分:0)

简而言之,抛开await。通过使用await,您实际上是在告诉它在这里完成,直到完成这件事为止。

如果要并行化它们,请使用Promise.all()。任何async函数都将返回一个Promise,它仍然可以像普通Promise一样使用。 Promise.all()接受Promise个对象的数组,所有这些请求完成后将调用then(),为您提供每个结果的数组。

您可以执行以下操作:

const urls = [/* bunch of URLs */];

Promise.all(
 urls.map(url => 
   fetch(url).then(res => res.text())
 )
).then(results => /* do something with results */)

在这种情况下,results将是您各种请求的结果的数组,其顺序与传递的顺序相同。

现在,如果您希望一次运行特定数量的设备,则需要对其进行一些更改,并对运行情况有所限制。

我通常使用一种技术,该技术仅使用一个简单的计数器来跟踪有多少活动状态,然后在完成后触发更多活动。

您可以执行以下操作:

// dummy fetch for example purposes, resolves between .2 and 3 seconds
const fakeFetch = url => new Promise(resolve => setTimeout(() => resolve(url), Math.random() * 2800 + 200));

const inputUrls = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
const limit = 2; // this sets the limit of how many can run at once, set to 10 to run 10 concurrently
const delay = 100; // delay in ms between each batch starting

function fetchAll(urls) {
  let active = 0;
  let queue = urls.slice(0); // clone urls
  
  // inner function so urls and results can be shared with all calls
  function fetchAllInner() {
    if (active < limit && queue.length) {
      const count = Math.min(limit - active, queue.length);
      const urlsThisBatch = queue.slice(0, count);
      queue = queue.slice(count); // remaining
      
      return Promise.all(
        urlsThisBatch.map(url => {
          active++; // increment active
          console.log('start', url);
          return fakeFetch(url)
            .then(r => {
              console.log('done', url);
              active--; // decrement active
              return new Promise(resolve => // new Promise to promisify setTimeout 
                setTimeout(() => 
                  resolve(fetchAllInner() // kicks off run again when one finishes
                    .then(fetchR => [].concat(r, fetchR)) // combine them
                  ), delay
                )
              );
            })
        })
      ).then(r => r.reduce((a, u) => [].concat(u, a), [])); // flatten from Promise.all()
    }
    
    return Promise.resolve([]); // final resolve
  }
  
  return fetchAllInner();
}

fetchAll(inputUrls)
  .then(results => console.log('all done', results));

简而言之,这是为批次创建一个Promise.all()(但是我们可以启动很多,直到达到极限)。然后,当一个操作完成时,它将通过递归调用相同的函数来设置超时时间以启动另一个批处理。它包装在另一个函数中只是为了避免必须将某些变量设置为全局变量。

如果需要的话,这还会增加延迟,因此您可以限制要发出的请求数量,而不会影响系统。如果您不想使用延迟,则可以将其设置为0或删除new Promise(resolve => setTimeout位。

上面的版本有点冗长,以便于理解。这是一个更加“生产就绪”的版本(请确保将fakeFetch切换为fetch并处理调用res.text()

const fakeFetch = url => new Promise(resolve => setTimeout(() => resolve(url), Math.random() * 2800 + 200));

function fetchAll(urls, limit = 10, delay = 200) {
  let active = 0;
  const queue = urls.splice(0);

  function fetchAllInner() {
    if (active >= limit || !queue.length) {
      return Promise.resolve([]);
    }
    
    const count = Math.min(limit - active, queue.length);
    active = limit;
    
    return Promise.all(
      queue.splice(0, count)
        .map(url => fakeFetch(url)
          .then(r => {
            active--;
            return new Promise(resolve =>
              setTimeout(() => resolve(
                fetchAllInner().then(fetchR => [].concat(r, fetchR))
              ), delay)
            );
          })
        )
      ).then(r => 
        r.reduce((a, u) => [].concat(u, a), []));
  }
  
  return fetchAllInner();
}

console.log('give it a few seconds');
fetchAll(['a', 'b', 'c', 'd', 'e', 'f', 'g'])
  .then(r => console.log('all done', r))