async/await의 병행·직렬 실행을 측정하면서 이해하기-JavaScript

35197 단어 JavaScripttech

개시하다


본 보도의 목적은 async/await 문법을 사용하는 비동기 처리를 깊이 있게 이해하고 병렬, 직렬로 연결할 때 어떻게 기술하는 것이 좋을지 기술하는 것이다.

주의 사항


방법체인을 사용해 간단한 기술방법을 사용할 수 있도록 기존 유형에 확장방법을 추가하는 방법을 적용한 만큼 지속 활용 여부는 사업 가이드라인에 적합한지에 달렸다.
만약 사용하지 않는 방법을 사용한다면 이전의 방법으로 한 번 묘사하는 것을 추천합니다. 읽기가 매우 힘들 것 같습니다.

전지식


인상은 전류이고 직렬 집행은 배터리의 직렬 집행이며 병렬 집행은 병렬 연결이어서 이해하기 쉽다.

직렬 실행


serial.png
직렬 실행은 처리 결과가 전파되어야 할 때 주로 사용된다.
Promise의 then() 방법은 아래의 처리를 받아들여 새로운 Promise로 되돌아오기 때문에 방법 체인으로 기술할 수 있다.
const p = asyncHoge()
  .then(fooFromHoge) // 同期処理も可
  .then(asyncPiyoFromFoo);
console.log(await p);

병렬 실행


parallel.png
병렬 실행은 전파가 필요 없는 처리를 동시에 실행하는 데 사용됩니다.
주로 Promise.all() 방법을 사용하는데 그 중에서 여러 과정을 포함하는iterable(배열 등)가 전달되고 await를 통해 해결된 결과를 수조로 받아들인다.
const p = Promise.all([
  asyncHoge(),
  foo(),
  asyncPiyo(),
]);
console.log(await p);

병렬 블렌드 실행


serial_parallel.png
혼합 모델은 복잡한 구조로 직렬은 때로는 병렬로 나뉘고 병렬은 때로는 각 요소를 합치며 내부의 각 요소도 귀속되는 구조이다.
// コードは複雑になるため、拡張メソッドを使った方法を後述していきます。

측정 코드


코드를 쓸 때 다음과 같은 확장 방법을 실시하였다.
Function.prototype.as_async = async function(...args) { return this(...args); };

Promise.prototype.map = async function(fmap) {
  const f = async x => fmap(await x);
  return [].concat(await this).map(f);
};
Promise.prototype.as_promise_all = async function() {
  return Promise.all([].concat(await this));
};

Array.prototype.as_async = async function() { return this; };
이렇게 하면 필요한 awaitawait Promise.all([...])를 제거할 수 있고 방법 체인을 통해서만 직렬과 병렬의 혼합 구조를 기술할 수 있다.
측정할 때, 비동기 함수는 실행할 때 임의의 delay 값을 설정합니다.
// 実行する関数 ---
// ----------------
async function sleep(ms) {
  const p = new Promise(resolve => setTimeout(resolve, ms));
  await p;
}

async function asyncN(v) {
  await sleep(v*100);
  return v;
}
async function asyncx2(v) {
  await sleep(v*200);
  return v*2;
}
async function asyncx2inv(v) {
  await sleep((10-v)*200);
  return v*2;
}

function N(v) { return v; }
function x2(v) { return v*2; }

function sum(a) { return a.reduce((sum, elem) => sum + elem, 0); }
function mul(a) { return a.reduce((sum, elem) => sum * elem, 1); }
이외에 측정용 기준 함수도 준비했다.
// ベンチ用 ---
// ------------
async function bench(f) {
  const start = new Date();
  console.log('result:', await f());
  console.log('elapsed:', (new Date()).getTime() - start.getTime(), 'ms');
}
그러면 직렬 연결, 병렬, 직렬 연결과 병렬 혼합의 각 처리를 쓰십시오.

직렬연결


병행과 비교하기 편리하도록 비동기 처리의 배열을 사용하여 측정하기 때문에 엄밀히 말하면 함수는 각 병렬 처리를 직렬로 연결하는 것이다.
//                        100 ms,    200 ms,    300 ms
const src_ary = () => [asyncN(1), asyncN(2), asyncN(3)];
// 4300 ms
await bench(async () =>
  src_ary().as_async().as_promise_all() //  100 ms,  200 ms,  300 ms
    .map(x2).as_promise_all()
    .map(asyncx2inv).as_promise_all()   // 1600 ms, 1200 ms,  800 ms
    .map(asyncx2).as_promise_all()      //  800 ms, 1600 ms, 2400 ms
    .map(x2).as_promise_all()
);
각 병행 처리 결과를 기다려야 하기 때문에 측정 시간은 각 병행 처리의 각 최대치를 더한 결과로 상기300 + 1600 + 2400이기 때문에 4300ms의 시간이 필요하다.

병렬하다


병행 평가는 한 차례Promise.all()만 진행된다.
// 3500 ms
await bench(async () =>
  src_ary().as_async() //  100 ms,  200 ms,  300 ms
    .map(x2)
    .map(asyncx2inv)   // 1600 ms, 1200 ms,  800 ms
    .map(asyncx2)      //  800 ms, 1600 ms, 2400 ms
    .map(x2)
    .as_promise_all()
);
하나하나 처리를 수행할 수 있기 때문에 측정 시간은 각자의 합계 시간의 최대치이고 상기max(2500, 3000, 3500)이기 때문에 3500ms의 시간이 필요하다.

직렬 및 병렬 블렌드


마지막으로, 시작의 직렬과 병렬의 혼합 실행을 실현해 봅시다.
// 4600 ms
await bench(async () =>
  // a1_1()
  asyncN(2)        // 200 ms
    // a1_2()
    .then(asyncx2) // 400 ms
    .then(x => // 4
      [
        [
          // a2A()
          asyncN(x) // 400 ms
            .then(y =>
              [
                // a2AA
                asyncN(y),        // 400 ms

                // a2AB_1
                asyncN(y)         // 400 ms
                  // a2AB_2
                  .then(asyncx2), // 800 ms
              ]
                .as_async()
                .as_promise_all()
                  // a3_1
                  .then(sum) // [4, 8] -> 12
                  // a3_2
                  .then(asyncx2)  // 2400 ms
            ),

          // a2B_1
          asyncN(x + 1) // 500 ms
            // a2B_2
            // -> [1, 2, 3, 4] 
            .then(y => {
              let a = [];
              for (let i = 1; i < y; i++) {
                a.push(i);
              }
              return a;
            })
            // a2B_3
            .map(y => y*y)
              .as_promise_all()
            .then(za => // [1, 4, 9, 16]
              [
                // a2BA_1
                sum.as_async(za) // -> 30
                  /// a2BA_2
                  .then(asyncx2inv), // 0 ms

                // a2BB
                mul.as_async(za), // -> 576
              ]
                .as_async()
                .as_promise_all()
            ),
        ]
          .as_async()
          .as_promise_all()
          // a5
          // NOTE: 直列と並列で `[ a3_2(), [a2BA_2(), a2BB()] ]` となるので
	  //       flattenして渡す
          .then(a => sum(a.flat())), // [24, [60, 576]].flat() -> 660

        // a2C
        asyncN(x + 2) // 600 ms
          .then(y =>
            [
              // a2CA_1
              asyncN(y)         // 600 ms
                // a2CA_2
                .then(asyncx2), // 1200 ms

              // a2CB
              asyncN(y),        // 600 ms
            ]
              .as_async()
              .as_promise_all()
              // a4
              .then(mul) // [12, 6] -> 72
              ,
          )
      ]
        .as_async()
        .as_promise_all(),
    )
    // a6
    .then(sum) // [660, 72] -> 732
);
이쪽의 측정 시간은 다음과 같다.
serial_parallel_time.png
상술한 계산을 참고하다
200 + 400 +
  max(
    max(
      400 + max(400, 400 + 800) + 0 + 2400,
      500 + 0 + 0 + max(0 + 0, 0)
    ) + 0,
    600 + max(600 + 1200, 600) + 0
  ) + 0
, 4600ms가 소요됩니다.
또한 병목과 가장자리가 있음을 알 수 있다. 병목a3_2()에 해당하는 처리는 2400ms가 필요하기 때문에 이곳을 단축하면 전체적인 집행 시간이 짧아진다. 반대로 a2CA_2()에 해당하는 처리는 1200밀리초가 필요하다.이곳은 아무리 짧아도 실행 시간은 변하지 않는다.
또한 a2A()부터 시작된 직렬 처리는 4000ms를 바꿀 수 없다. 예를 들어 a4()에 대응하는 처리는 1600ms의 여분이 있다.

전체 텍스트 인코딩


길어져서 외부 링크예요게재 코드가 포함된 전문과 실행 결과는 다음과 같다(Wandbox) 확인 가능합니다.

좋은 웹페이지 즐겨찾기