同期・非同期用語解説

【JavaScript】 並列処理と非同期/同期処理の違い

更新日:2023/02/06

JavaScriptでプログラムをしていると非同期処理を頻繁に使用します。
そのため非同期処理の知識が必要なのですが、知らなくてもコードは組めます。
だからあまり気にしていなかったのですが、同じような言葉に並列処理というものがあることに最近気が付きました。

そうなると違いが気になってきます。
そこで並列処理と非同期処理および同期処理の違いについて、調べてみました。

 

並列処理とは

プログラム用語での並列処理は、複数の処理を同時に実行することを指します。

コンピューター単位での並列処理

元々コンピューターのCPUは、プログラムコードを順番に一つ一つ処理するように設計されていました。
そのため別のプログラムコードを実行するには、現在実行されているプログラムが終了するのを待たなければいけません。

そこで一台のコンピューター内で複数のCPUを組み込んだり、短い時間で処理を切り替えるなどの手法で、複数のプログラムを同時に動作できるようになりました。
これにより、コンピューターの時間当たりの処理量が増え、ユーザーの利便性が向上したのです。

並列処理 昔と今

プログラムコードを一つ一つ処理する方式を逐次処理といいます。
この処理の単位をスレッドと呼びます。

その逐次処理を同時に複数動作させる方式が並列処理です。

コンピューターのハードウェアスペックとしてのスレッドと、OSやプログラミング上のスレッドは意味合いが少し異なります。

ハードウェアスペックとしてのスレッドは同時に処理できるスレッドの数で、CPUコア数や1つのコアに対して割り当て可能なスレッドの数などで計算されます。
OS上ではそれ以上のスレッドが動作しているように見えますが、処理を細かくスケジューリングしてCPUに割り振っています。

プログラム単位での並列処理

現在のコンピューターはOSの管理のもと、複数のプログラムが並列で動作しています。

プログラムの実行単位をプロセスと呼びます。
基本的には1プロセス1スレッドで動作しますが、一つのプロセスが複数のスレッドを管理することも可能になっています。
つまりプログラム毎に、複数の処理を並列で実行できるのです。

プログラム単位での並列処理

例えば、Webページを閲覧するために使用されるブラウザは、タブごとにスレッドが作成され、個々にサーバーへのアクセスや画面表示がおこなわれています。

プロセスは独自のメモリ領域を割り当てられていて、同じプロセスが管理するスレッド間ではメモリを共有できます。
その反面、プロセス間ではメモリを共有できない仕組みになっています。

同じプロセスが管理するスレッド間ではメモリを共有できる

JavaScriptでの並列処理

JavaScriptは基本的に一つのスレッド(シングルスレッド)で処理されます。
シングルスレッドは時間がかかるI/O処理、例えばWebサーバーからのデータ取得などをおこなっている間は他の処理ができません。

JavaScriptでは処理待ちで他の処理ができないことをブロッキングと呼び、できるかぎり回避するように設計されています。
その手法として、データの送受信などを他のスレッドなどに丸投げして並列処理することで、他の処理を実行することができるようになっています。

このような処理は内部的におこなわれます。
そのため、並列処理を意識することなくプログラミングをおこなうことができます。
(その代わりに非同期処理を意識する必要があります)

ただし、ブラウザに組み込まれているWeb Worker APIなどを利用することでプログラムコードレベルでサブスレッドを作成でき、複数のコードを並列処理可能になっています。

 

同期/非同期処理とは

コンピューター用語としての同期は、様々なケースで使用されます。
例えば、ハードディスクのデータを他の媒体にバックアップしているとき、同じ内容を保つことを『同期する』といったりします。
ネットワークでの通信も、相手の応答を待って送信するなどタイミングを合わせてデータのやり取りをします。
このタイミングを合わせることを『同期する』といったりします。

非同期は文字通り『同期しない・させない』という意味です。
基本的には同期していないものを同期させるために様々な規格などを制定していることが多いです。
そのため非同期という言葉はあまり使われません。
しかしプログラミング用語としては、頻繁に使用されます。

プログラミング用語としての同期/非同期処理

異なるプロセスやスレッド間で、それぞれが持っている処理を順番に実行したいケースがあります。

例えば別のスレッドが持っているfunc2関数を実行し、その終了を待ってから次の処理へ進めるなどです。

一般的な同期のイメージ

このように終わるまで待って、次の処理を進めることを『同期する』といったりします。

一方、非同期処理は別のスレッド関数の終了を待たずに処理を続行します。
そして別スレッド関数が終了したら、関連する処理を実行します。

一般的な非同期のイメージ

同期中は処理が停止しています。
プログラムによっては、使用者に悪いイメージを与えてしまうことがあります。
GUIを持つプログラムで操作を受け付けないなどです。

そのためメインとなるスレッドは同期で操作を止めるのではなく、非同期で結果を受け取るようにプログラミングすることがあります。

■非同期も同期と言える

上記の非同期の考え方は、別スレッドの終了を待っている間に他の処理をやっているだけで、同期しながら一連の処理をしているとも言えます。

しかしプログラムコードの流れとしては連続していません。
そのためプログラムコードとしては非同期なのです。

少し目線を変えるだけで同期か非同期かの認識が変わってしまうのが、とてもわかり難い原因なのです。
深く考えずに、『そういうものなんだ』と思っておいた方がいいかもしれません。

JavaScriptにおける同期/非同期処理

JavaScriptでは、プログラムコードが連続して実行されるとき『同期処理』と呼びます。

そして、タイマーによる時間経過後やブラウザのボタンクリックによるの関数呼び出しなど、後で実行される処理を『非同期処理』と呼びます。

次のコードは、同期処理で最初から最後まで順番に処理されます。
mapメソッドに渡している関数も、mapメソッド内で同期的に実行されています。

同期処理の例


const a = [ 1, 2, 3 ];
const b = 3;
const c = a.map( 
      e=> e * b  // 同期的に実行される関数
    );
console.log( c );

次のコードは非同期処理の例ですが、最初から最後まで順番に処理されるので同期処理です。
しかしsetTimeoutで渡している関数は、指定時間経過後に実行されます。
つまり、この関数が非同期で実行される処理です。

非同期の例


const a = [ 1, 2, 3 ];
const b = 3;
setTimeout(
    ()=>{ // 非同期で実行される関数
        const c = a.map(
            e=> e * b
        );
        console.log( c );
    },
3000);

非同期とイベントループ

非同期処理はイベントループで通知されます。

非同期で実行される関数(コールバック関数等)は、プログラムコード実行の過程でWebAPIなどに渡されます。
渡された側はタイマーが経過したりマウスクリックされたりなどの条件が満たされると、タスクキューに次に実行してほしい処理をタスクとして登録します。

JavaScriptは、今処理している一連のプログラムコードが終了したら、タスクキューにタスクがないか確認します。
タスクがある場合は一番古いタスクが実行されます。
このタスク内で非同期で実行される関数が呼び出されます。

タスクの処理が終わったらキューから削除し、次のタスクを処理します。

全てのタスクが終了したら、新たにタスクがキューされるのを待ちます。
このタスクを確認して処理するループをイベントループと呼びます。
非同期処理はイベントループで通知されるのです。

イベントループの処理イメージ

実際には複数のキューが用意されていて、優先順位の高いタスクを優先順位の高いキューに割り振るなど、効率を考慮した設計がされています。

注:
イベントループや非同期処理の仕組みはJavaScriptの言語仕様(ECMAScript)で定義されているものではありません。
ブラウザはHTML Standardという標準的な規格があり、この中で定義されています。
サーバーサイドJSも、それぞれ独自の仕様で実装されています。

同期構文は同期していない

JavaScriptには非同期処理を同期処理として記述するasync/awaitという構文があります。

これは、非同期処理をプログラムコード上で同期しているかのように記述したものだったりします。
実際には同期していないので、同期的処理の方が意味合い的には正しいです。

次のコードはasync/awaitで非同期処理を同期処理で記述したものです。


window.addEventListener( "DOMContentLoaded" , ()=> {

    let count;
    document.getElementById("b1").addEventListener( "click",e=>{
        console.log( "click" );count++;
    });

    (async ()=>{
        count = 0;
           // 同期処理
        await new Promise( resolve => setTimeout( resolve , 5000 ) );
        console.log( "5秒経過(" + count + ")"  );
    })();

});

※DOM要素としてIDがb1のbuttonタグがあるとします

setTimeoutは指定時間経過後に指定された処理をおこなう非同期処理関数です。
これをPromise化して、async関数の中でawaitキーワードを使用することで同期処理として記述できます。

実行すると5秒間停止してから、コンソールに"5秒経過(0)"と表示されています。
停止しているので、変数countも0のままですね。

これは勘違いだったりします。
実際には停止しておらず、5秒経過待ち中にボタンを押すと"click"と表示されます。
さらにcountの値も変更されています。

実はJavaScriptのシステムが、非同期で5秒待ち、経過したら次の処理をおこなうように、コード組み替えているのです。

ここで問題なのが、変数countが更新されていることです。
同期なら外部から更新されることがありません。
しかし非同期なので、思わぬ落とし穴になります。

同期ではなく、非同期と認識してコードを作成する必要があります。

更新日:2023/02/06

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。