【同期・非同期】PHPとnode.jsの違いについて

最近サーバーサイドはnode.jsを使っていたのですが、久しぶりにPHPに舞い戻りました。
PHPの記憶を失いつつあるので、迫っている応用情報の勉強もかねつつnode.jsとの処理の違いについてまとめたいと思います。

PHPとnode.jsは何が違う?

一言でいうと同期処理か非同期処理かです。

PHPは基本的にコードの上から順に処理を実行する=同期処理です。
非同期処理も可能らしいですが、工夫が必要です。

一方、node.jsは

スケーラブルなネットワークアプリケーションを構築するために設計された
非同期型のイベント駆動の JavaScript 環境

と公式で説明されています。

プロセスとスレッド

同期・非同期について説明する前に、プロセスとスレッドについてまとめます。

プロセスとは

プロセスとは、プログラムの実行中の状態を指します。
実行中の状態とは、プログラムがOSによって、メモリ上の領域を割り当てられ、そこでCPUとデータのやり取りを行い、演算処理されている状態です。

タスクマネージャを開くとchromeやdockerなど実行中のアプリが出てきますが、それらが別々のプロセスになります。プロセス同士はメモリを共有することはなく、互いに影響を及ぼしません。

余談ですが応用情報だとタスクという言葉で表現されているらしいです(厳密には違うらしいですが…)

スレッドとは

スレッドとは、CPUから見たプログラムの「実行単位」であり、プロセスに含まれます

スレッドを複数持つプロセス・・・マルチスレッド
スレッドを1つだけ持つプロセス・・・シングルスレッド
と呼ばれます。
PHPとnode.jsは、いずれもシングルスレッドとなります。

マルチスレッドのメリット・デメリット

並列処理が簡単に実現できます。
ただし、スレッド間ではプロセスに割り当てられたメモリを共有するため
リソースの管理をしっかりと行わないと、再現性の低いバグ(稀に起こるバグ)を発生させる危険性があるので、注意が必要です。

ちなみに、 マルチスレッドの環境で実行されても内部データへのアクセスが競合せずに、常に内部データの整合性が保たれているように対策されている状態をスレッドセーフといいます。

シングルスレッドのメリット・デメリット

マルチスレッドで考慮すべきことを考えなくてすむのがメリットです。
一方、一度に1つの処理しかできないため、ものすごく重い処理をしているときに画面更新が行われず画面がフリーズしているように見えます。

PHP+apacheの問題点

PHPはApacheを使用してマルチプロセスモデルとして運用することが一般的です。
Apacheはクライアントの接続一つ一つに対してプロセスを生成する仕様となっています。
ただしPHP+apacheにはデメリットがあります。
PHPの同期I/Oの問題点
PHPは先述したとおりシングルスレッド、同期I/Oです。
同期I/Oとは、I/O(入出力)処理の間、プログラムを停止(ブロッキング)してI/O処理を待たなければならないことを言います。
同期I/Oはブロッキングモデルともいいます。従来のプログラミング言語は大体ブロッキングモデルです。
今行っている処理が終わってからでないと次の処理に手を付けられないため、処理速度は遅くなります。

C10K問題

PHP+Apacheのマルチプロセスモデルの問題点はC10K問題とも言われます。
C10K問題とは「クライアント1万台問題」とも呼ばれるもので、
サーバーのハードウェア性能には問題がなくてもクライアントの同時接続数が増えるとサービスの応答が遅延するというものです。

node.jsの非同期I/O

 
C10K問題を解消するのがnode.jsの非同期モデルです。
node.jsは

・シングルスレッド・シングルプロセス
・非同期I/O(ノンブロッキングモデル)

で動作します。
ただし、I/O処理がすべてノンブロッキングというわけではなく一部はブロッキングにも対応しています。
I/O処理であるファイルの読み取りのメソッドと、その後続処理moreWork()を例として、まずは従来型のブロッキングモデルから見ていきます。
 

node.jsのブロッキングモデル

const fs = require('fs');
const data = fs.readFileSync('/file.md'); // ファイルが読み込まれるまでここでブロック
console.log(data);
moreWork(); // console.logの後に実行
メソッド名にsyncと名前が付くのが特徴です。
moreWork()は、readFileSyncが正常終了されてconsole.log()が出力された後に実行されます。
ブロッキングモデルの場合、readFileSyncでエラーが出た場合そこでプロセスが中断し、後続の処理は行われません。
Node.jsはシングルスレッド/シングルプロセスが原則ですので、こういった処理を長時間行うと後続の処理がなかなか実行されずサービスがまともに提供できなくなる可能性があります。

そのため、後述のノンブロッキングモデルを用いるのが一般的です。

node.jsのノンブロッキングモデル

const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
moreWork(); // console.log の前に実行
ノンブロッキングモデルでは、moreWork()はconsole.log()の前に実行されます。
I/O による待ち時間は発生しておらず、後続処理moreWork()をブロックしていません。エラーが出た場合でも中断せずに次の処理に移ることができます。
Node.js はこの非同期 I/O を駆使して、時間軸で処理を分散することができます
 

結局非同期 I/O の何がうれしいの?

・処理の進捗をチェックしながら同時に複数の処理を並行して行える
・入力したデータに対して待ち時間がないため、大量のアクセスにも対応可
・マルチスレッドのめんどくさいことを考えなくても、並行(っぽく)処理ができる

また、同期I/OであるPHPは上記非同期IOの強みが弱点となるので実装を工夫していく必要があります。
実際にPHPがどのように大量アクセスに対応しているかというと、apacheではなくnginxを使うケースがあるようです。nginxは非同期IOに対応していて、apacheの10~100倍のアクセスに耐えられるそうです。

nodejsは便利ではありますが、まだ対応しているサーバーが少なかったりします。
やはり古めのサービスなどはapacheであることも多いので、その場合にどのように大量アクセスなどに対応していくかは今後調べていきたいと思います。

参考資料

https://blog.recruit.co.jp/rls/2019-12-13-node-async-io/
https://nodejs.org/ja/docs/guides/blocking-vs-non-blocking
https://dsinside.digitalstage.jp/entry/2021/03/15/171517

前へ

Goで負荷テストを書きたい[locust+boomer]

次へ

【Vue3】FullCalendarを使用して予定表を作ってみた