メインコンテンツまでスキップ

Process

Overview

言語ごとで比較される項目のプロセスやスレッドについてまとめているセクション。

マルチスレッドかマルチプロセスかと、並行処理か並列処理かの間に関係性はない。

  • マルチスレッドで並列処理
  • マルチプロセスで並行処理
  • マルチプロセスで並列処理

マルチスレッドを使う場合は並行処理か並列処理かによらずスレッドセーフかどうかを考える必要がある。 並列処理ができるかどうかはCPUのコア数に依存する。コア数が2以上であれば並列処理ができる。

前提

プログラムの並列処理にはマルチスレッドマルチプロセスの2種類がある。

  • マルチスレッド 同じプロセス内で動作し、同じメモリ空間を共有するため、スレッド間でのデータ共有を効率的に行うことができる。
  • マルチプロセス プロセスごとに独立したメモリ空間を持ち、プロセス間通信ができる。

並行処理と並列処理とは

Image from Gyazo

並行処理と並列処理の違いは、タスクの分割・実行において、同時に行われるか否か。

  • 並行処理 複数のタスクを同時に処理することを指し、処理Aと処理Bが交互に実行されるイメージ。
  • 並列処理 複数のタスクを同時に実行することを指し、処理Aと処理Bが同時に実行されるイメージ

プロセスとスレッドの違い

  • プロセス メモリ空間やCPU時間など、システムリソースを割り当てられた実行単位。 プロセスは複数のスレッドで構成されることがある。
  • スレッド プロセス内で実行される、より小さな実行単位。

プロセスは独立した実行環境を持っており、別のプロセスとは独立に動作する。 一方で、スレッドは同じプロセス内で共有されるメモリ空間やファイルハンドル、その他のシステムリソースを共有している。つまり、スレッドはプロセス内での並行処理を行うための単位であり、複数のスレッドが同時に実行されることがあるもの。

マルチスレッドとマルチプロセス

「マルチスレッドとマルチプロセス」と「並行処理と並列処理」は別々で考えないといけないよ

  • マルチスレッドとは 1つのプログラムを複数に分割し、同時に処理を進める技術のこと。 つまり、1つのプログラム内で複数の処理を同時に実行できる。 スレッドは軽量で同じプロセス内であるため、スレッド同士でのデータの共有が容易。 また、プロセスの生成や破棄が必要なく、プログラマの手間も少なくて済む。 ただし、スレッド同士が同じデータにアクセスするときには、注意が必要で、スレッドの中で排他制御を行わなければデータ競合が起こる可能性(スレッドセーフの話)
  • マルチプロセスとは 1つのプログラムを複数に分割し、複数のプロセスで同時に処理を進める技術のこと。 つまり別々のプログラム間(別々の実行環境)でデータをやりとりしながら処理を進めることができる。 プロセスは重量で、まったく別のプロセスとして実行されるため、プロセス間でデータの共有には特殊な手法が必要であり、プロセスの生成や破棄には時間とリソースが必要。

マルチプロセスとマルチスレッドの比較

項目マルチプロセスマルチスレッド
並列処理の単位プロセスごと1つのプロセス内の複数スレッド
メモリ共有共有されない共有される
独立性高い(独立したメモリ空間)低い(メモリ空間を共有)
オーバーヘッド高い(プロセス生成コストが高い)低い(スレッド生成は軽量)
用途PHP、ApacheなどNode.jsのイベントループ、Javaなどの並列処理

マルチプロセスモデル

マルチプロセスモデルとは、リクエストごとに独立したプロセスを立ち上げて処理する方式です。そのため、1つのリクエストが1つのプロセスに対応し、そのプロセスは1つのスレッド(シングルスレッド)で処理される。

各言語の基本モデルと並行処理方法

それぞれの言語は異なる並行処理モデルを採用しており、用途やユースケースに応じた選択が重要。

PHP

  • 基本モデル
    • マルチプロセス
  • 特徴
    • リクエストごとに新しいプロセスを生成する
    • 1プロセスは1スレッドで処理
    • メモリ空間はプロセスごとに独立しているため、並列処理は「プロセス」単位
    • ApacheやNginx + PHP-FPMで各リクエストにプロセスを割り当てる
  • 利点
    • リクエストがプロセス単位なので他のリクエストの影響を受けにくい
  • 欠点
    • プロセスの生成コストが高く、多数のリクエストに対してメモリ効率が悪い

JavaScript(ブラウザ)

  • 基本モデル
    • シングルスレッド + イベントループ(非同期処理)
  • 特徴
    • JavaScriptはシングルスレッドで動作
    • 非同期処理(Promise、async/await、setTimeoutなど)で並行処理を実現
    • Web Workersを使えばマルチスレッド処理が可能
    • fetch() による非同期通信でブロッキングを回避
  • 利点
    • 非同期処理を効率的に管理でき、スレッド競合が発生しない
  • 欠点
    • CPU負荷の高い処理には不向き(シングルスレッドのため)

Node.js

  • 基本モデル
    • シングルスレッド + 非同期イベントループ
  • 特徴
    • JavaScriptと同様にシングルスレッド
    • 非同期処理によりスループットを高める
    • worker_threads によるマルチスレッド対応も可能(ただし一般的ではない)
  • 補足
    • Node.jsは1プロセス・1スレッドで複数リクエストを処理するイベントループ型
    • 非同期I/O処理中も他の処理にスレッドを使い続けられるため高効率
    • CPUバウンドな処理は別スレッドや外部に分離するのが理想
  • 利点
    • IO待ちが多い処理(DB・ネットワーク等)に強い
  • 欠点
    • CPU集約処理には弱い

Java

  • 基本モデル
    • マルチスレッド
  • 特徴
    • 標準でマルチスレッド対応
    • スレッドプールやExecutorServiceで並行処理を制御
    • スレッド間の同期やロックが必要
  • 利点
    • 並列処理やCPU集約処理に強い
  • 欠点
    • データ競合対策としてロック設計が必須

Ruby

  • 基本モデル
    • マルチスレッド(ただしGILの制約あり)
  • 特徴
    • MRI実装ではグローバルインタプリタロック(GIL)で同時実行は1スレッド
    • 非同期IOではスレッド切替が可能
    • JRubyではGILがなく完全なマルチスレッドが可能
  • 利点
    • 簡易的にスレッドを導入できる
    • Railsなどで非同期処理を組み込みやすい
  • 欠点
    • MRIでは並列処理のパフォーマンスが制限される

Ruby on Rails(フレームワーク)

  • 基本モデル
    • マルチプロセス + マルチスレッド
  • 特徴
    • Pumaなどで複数プロセス・スレッドを立ち上げて並行処理
    • スレッドセーフな設計が求められる
  • 利点
    • 複数リクエストを効率的に処理可能
  • 欠点
    • スレッドセーフでないコードは競合を起こす

Flutter(Dart)

  • 基本モデル
    • マルチスレッド(Isolate)
  • 特徴
    • Dartはスレッドの代わりにIsolateを使って並行処理
    • 各Isolateは独立したメモリ空間を持つ
    • メッセージパッシングで通信
  • 利点
    • スレッド間競合がなく、シンプルな並行処理が可能
  • 欠点
    • メッセージ通信のオーバーヘッドがある

言語の並行処理モデル表

ポイント PHPは1プロセス1スレッドで処理されるため、プロセスごとにメモリが分離されており、並列処理はスレッドではなくプロセス単位。
Node.jsやJavaScriptは非同期処理を使ってシングルスレッド内で効率的に並行処理を行う。
JavaやFlutterのように、マルチスレッドを活用した本格的な並行処理が可能な言語もある。 Rubyの標準実装はGILの影響で同時に動けるスレッド数が制限されますが、非同期IOでは他スレッドを実行可能。

言語/フレームワーク並行処理モデル詳細
PHPマルチプロセスリクエストごとにプロセスを生成。各プロセスは1つのスレッドのみで処理する。
JavaScript (ブラウザ)シングルスレッド + 非同期イベントループJavaScriptのランタイムはイベントループを用いて非同期処理を並行処理するが、スレッドは1つのみ。Web Workersを使用すればマルチスレッド処理が可能。
Node.jsシングルスレッド + 非同期イベントループ + Worker Threads(マルチスレッド対応可)Node.jsは非同期処理で並行処理を実現するが、デフォルトではシングルスレッド。worker_threadsを使えばマルチスレッド処理が可能。
JavaマルチスレッドJavaはデフォルトでマルチスレッド対応。スレッドプールや並列処理用のライブラリが充実している。
RubyマルチスレッドRubyはマルチスレッド対応だが、MRI(標準実装)は**グローバルインタプリタロック(GIL)**により、1つのスレッドしか同時実行されない。ただし、IO待ちなど非CPU処理の際には別スレッドが動く。
Ruby on Railsマルチプロセス + マルチスレッドPumaなどのアプリケーションサーバーで並列処理を行う。マルチスレッド対応もしているが、スレッドセーフなコードが必要。
Flutter (Dart)マルチスレッドDartはIsolateと呼ばれる軽量なスレッドを使用して並行処理を行う。各Isolateはメモリ空間を共有せず、メッセージで通信する。

シングルスレッド + グローバルスコープの危険性

Node.jsのようなシングルスレッド環境では、すべてのリクエストが1つのスレッド上で非同期的に処理される。
そのため、グローバルスコープにユーザー情報などのリクエスト依存のデータを保持してしまうと、
複数リクエストの処理が重なった際に、情報が上書きされたり、他ユーザーの情報が漏洩したりする危険がある。

例(危険な実装):

// グローバル変数にリクエストごとの情報を保存(NG)
let currentUser = null;

app.use((req, res, next) => {
currentUser = req.user;
next();
});

app.get('/me', (req, res) => {
res.json({ user: currentUser }); // 他リクエストのユーザーが混在する可能性あり
});

このような実装は、他の非同期リクエストとの競合が発生するため、セキュリティ上大きな問題となる。

安全な代替案

  • req オブジェクトにユーザー情報を保存する(Expressなど)
  • NestJSでは Scope.REQUEST を使用してプロバイダごとにインスタンスを分離
  • Node.jsの AsyncLocalStorage を使って非同期コンテキスト内にデータを保持する

これらの方法を用いることで、リクエストごとの状態を安全に管理し、情報漏洩を防止できる。