جاوااسکریپت - محدود کردن تعداد پرامیسهای باز در یک زمان مشخص


کد TypeScript زیر هر فراخوانی به doSomething(action) را به صورت تکتک انجام میدهد. (یعنی مورد دوم در لیست فراخوانی نمیشود تا مورد اول تکمیل شود).

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

این یکی تمام درخواستها را بلافاصله به سرور ارسال میکند (بدون انتظار برای هیچ پاسخی):

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

اما چیزی که واقعاً نیاز دارم روشی برای کنترل آنها است. شاید همزمان ۱۰ یا ۲۰ تماس باز داشته باشم. (یکی یکی خیلی کند است، اما همهٔ ۶۰۰ تماس سرور را بارگذاری میکند.)

اما من در فهمیدن این مشکل دارم.

هر پیشنهادی برای این که چگونه میتوانم تعداد تماسها به X را که همزمان باز هستند محدود کنم وجود دارد؟

(این سؤال از TypeScript استفاده میکند، اما جواب به JavaScript ES6 هم برای من مناسب است.)

پاسخ شماره ۱

میتوانید این کار را در یک تابع کوتاه انجام دهید. (مقادیر را به ترتیب طبق پیشنهاد مولان برمیگرداند. ممنون!)

/**
 * 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));

پاسخ شماره ۲

ویرایش

جف بومن پاسخ خود را به طور قابل توجهی بهبود داده است تا مقادیر معنادار را حل کند. آزاد هستید که تاریخچه این پاسخ را مشاهده کنید تا بفهمید چرا این مقادیر حلشده بسیار مهم و مفید هستند.

گاز

این راهحل به طور نزدیکی از Promise.all بومی تقلید میکند

چطور یکسان است …

  • وعدهها را هر چه سریعتر برطرف میکند
  • یک آرایه از مقادیر را به همان ترتیبی که ورودیها هستند حل میکند
  • به محض برخورد با یک رد، رد میکند

چگونه متفاوت است …

  • پارامتر شماره تعداد Promises را که همزمان اجرا می شوند محدود می کند
  • ورودی آرایه وعده سازندگان (thunks) را میپذیرد؛ نه وعدههای واقعی

// 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)
)

خروجی کنسول

ورودیها به ترتیب اجرا میشوند؛ نتایج حل شده به همان ترتیب ورودیها هستند

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]

استفاده عملی

بیایید به یک مثال کد عملیتر نگاه کنیم. این کد وظیفه دارد مجموعهای از تصاویر را از یک سرور دریافت کند. این طوری میتوانیم از throttlep برای محدود کردن تعداد درخواستهای همزمان به ۳ درخواست در هر بار استفاده کنیم.

// 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>]
})