javascript - Limitarea numărului de promisiuni deschise într-un anumit timp


Următorul TypeScript execută fiecare apel către doSomething(action) unul câte unul. (Asta înseamnă că al doilea element din listă nu primește un apel până când primul nu este terminat).

async performActionsOneAtATime() {
    for (let action of listOfActions) {
        const actionResult = await doSomethingOnServer(action);
        console.log(`Action Done: ${actionResult}`);
    }
 }

Acesta va trimite toate cererile către server imediat (fără a aștepta vreun răspuns):

async performActionsInParallel() {
    for (let action of listOfActions) {
        const actionResultPromise = doSomething(action);
        actionResultPromise.then((actionResult) => {
            console.log(`Action Done: ${actionResult}`);
        });
    }
}

Dar ceea ce am nevoie cu adevărat este o modalitate de a le limita. Poate să am 10 sau 20 de apeluri deschise în același timp. (Unul câte unul este prea lent, dar toate 600 vor suprasolicita serverul.)

Dar am dificultăți să rezolv asta.

Aveți sugestii despre cum aș putea limita numărul de apeluri către X deschise simultan?

(Această întrebare folosește TypeScript, dar aș fi bine și cu un răspuns în JavaScript ES6.)

Răspuns #1

Poți face asta într-o singură funcție scurtă. (Returnează valorile în ordine conform sugestiei lui Mulan. Mulțumesc!)

/**
 * Performs a list of callable actions (promise factories) so
 * that only a limited number of promises are pending at any
 * given time.
 *
 * @param listOfCallableActions An array of callable functions,
 *     which should return promises.
 * @param limit The maximum number of promises to have pending
 *     at once.
 * @returns A Promise that resolves to the full list of values
 *     when everything is done.
 */
function throttleActions(listOfCallableActions, limit) {
  // We'll need to store which is the next promise in the list.
  let i = 0;
  let resultArray = new Array(listOfCallableActions.length);

  // Now define what happens when any of the actions completes.
  // Javascript is (mostly) single-threaded, so only one
  // completion handler will call at a given time. Because we
  // return doNextAction, the Promise chain continues as long as
  // there's an action left in the list.
  function doNextAction() {
    if (i < listOfCallableActions.length) {
      // Save the current value of i, so we can put the result
      // in the right place
      let actionIndex = i++;
      let nextAction = listOfCallableActions[actionIndex];
      return Promise.resolve(nextAction()).then(result => {
        // Save results to the correct array index.
        resultArray[actionIndex] = result;
      }).then(doNextAction);
    }
  }

  // Now start up the original <limit> number of promises.
  // i advances in calls to doNextAction.
  let listOfPromises = [];
  while (i < limit && i < listOfCallableActions.length) {
    listOfPromises.push(doNextAction());
  }
  return Promise.all(listOfPromises).then(() => resultArray);
}

// Test harness:

function delay(name, ms) {
  return new Promise((resolve, reject) => setTimeout(() => {
    console.log(name);
    resolve(name);
  }, ms));
}

var ps = [];
for (let i = 0; i < 10; i++) {
  ps.push(() => {
    console.log(`begin ${i}`);
    return delay(`complete ${i}`, Math.random() * 3000);
  });
}

throttleActions(ps, 3).then(result => console.log(result));

Răspunsul #2

EDITEAZĂ

Jeff Bowman și-a îmbunătățit considerabil răspunsul pentru a rezolva valori semnificative. Simțiți-vă liber să vizualizați istoricul acestui răspuns pentru a înțelege de ce valorile rezolvate sunt atât de importante/utile.

accelera

Această soluție imită îndeaproape Promise.all nativ

Cum este la fel …

  • Rezolvă promisiunile cât mai repede posibil
  • Rezolvă un șir de valori în aceeași ordine ca și intrările
  • Respinge de îndată ce întâlnește un refuz

Cum este diferit …

  • Parametrul number limitează numărul de Promisiuni care rulează simultan
  • Intrarea de tip array acceptă creatori de promisiuni (thunks); nu promisiuni reale

// throttlep :: Number -> [(* -> Promise)]
const throttlep = n=> Ps=>
  new Promise ((pass, fail)=> {
    // r is the number of promises, xs is final resolved value
    let r = Ps.length, xs = []
    // decrement r, save the resolved value in position i, run the next promise
    let next = i=> x=> (r--, xs[i] = x, run(Ps[n], n++))
    // if r is 0, we can resolve the final value xs, otherwise chain next
    let run = (P,i)=> r === 0 ? pass(xs) : P().then(next(i), fail)
    // initialize by running the first n promises
    Ps.slice(0,n).forEach(run)
  })

// -----------------------------------------------------
// make sure it works

// delay :: (String, Number) -> (* -> Promise)
const delay = (id, ms)=>
  new Promise (pass=> {
    console.log (`running: ${id}`)
    setTimeout(pass, ms, id)
  })

// ps :: [(* -> Promise)]
let ps = new Array(10)
for (let i = 0; i < 10; i++) {
  ps[i] = () => delay(i, Math.random() * 3000)
}

// run a limit of 3 promises in parallel
// the first error will reject the entire pool
throttlep (3) (ps) .then (
  xs => console.log ('result:', xs),
  err=> console.log ('error:', err.message)
)

Ieșire console

Intrările sunt procesate în ordine; rezultatele rezolvate sunt în aceeași ordine ca și intrările

running: 0
running: 1
running: 2
=> Promise {}
running: 3
running: 4
running: 5
running: 6
running: 7
running: 8
running: 9
result: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Utilizare practică

Să analizăm un exemplu de cod mai practic. Acest cod are sarcina de a prelua un set de imagini de pe un server. Așa am putea folosi throttlep pentru a limita numărul de cereri simultane la 3 odată.

// getImage :: String -> Promise<base64>
let getImage = url=> makeRequest(url).then(data => data.base64, reqErrorHandler)

// actions :: [(* -> Promise<base64>)]
let actions = [
  ()=> getImage('one.jpg'),
  ()=> getImage('two.jpg'),
  ()=> getImage('three.jpg'),
  ()=> getImage('four.jpg'),
  ()=> getImage('five.jpg')
]

// throttle the actions then do something...
throttlep (3) (actions) .then(results => {
  // results are guaranteed to be ordered the same as the input array
  console.log(results)
  // [<base64>, <base64>, <base64>, <base64>, <base64>]
})