プロセスとスレッドについて調べた


0. はじめに

Goでgoroutineやchannelの実装に出くわしたときに、雰囲気で見過ごしていたので基礎から整理する。

1. プロセスとスレッド

プロセス

  • 独立のメモリ空間を保有している処理の単位。
    • プロセス間では基本的にメモリは共有されない。
  • 1つ以上のスレッドから構成される。

スレッド

  • 1つのプロセスに割り当てられたメモリ内で動作する処理の単位。
  • スレッド間ではメモリが共有される。
    • スレッド間では同じデータに簡単にアクセスできる。

シングルスレッドとマルチスレッド

  • シングルスレッドとは、処理を上から順番に実行していくこと。
  • マルチスレッドとは、処理効率を上げるなどの目的で、複数の処理を並行して行うこと。
    • 例えば、時間のかかる操作を別スレッドで実行することで、ユーザー操作の受付を中断することなくプログラムを動かすことが可能になる。

2. マルチプロセスとマルチスレッドの違い

  • どちらも処理を並行して行う技術。
  • マルチプロセスの場合、プロセス間では基本的にメモリは共有されない。異なるプロセスが同じメモリ上のデータにアクセスすることは基本的にはない。
  • マルチスレッドの場合、スレッド間ではメモリが共有される。スレッド間では同じデータに簡単にアクセスできる。

3. スレッドセーフ

  • マルチスレッドの場合、ある共有データへの複数のスレッドによるアクセスがあるとき、一度に1つのスレッドのみがその共有データにアクセスするようにして安全性が確保されており、問題が発生しないこと。
  • 関連語に、リエントラント、排他制御、アトミックなど。

引用:

3. 並行処理(Concurrency)と並列処理(Parallelism)の違い


引用:

並行処理(Concurrency)

  • 処理を細切れに分割してコンテキストスイッチを繰り返しながら並行にタスクをこなしていく。
  • ある時間の範囲において、複数のタスクを扱うこと。

並列処理(Parallelism)

  • 複数の処理が同時に並列に実行される。
  • ある時間の点において、複数のタスクを扱うこと。

4. コンテキストスイッチについて

  • 1つのCPUが複数のプロセスを並行処理する(処理するプロセスを切り替える)ためにそれまでの処理の内容を記録し、新しい処理の内容を復元すること。
    • マルチプロセス(1リクエスト1プロセス方式)の場合
      • リクエストが増えるとプロセスも増えるため、コンテキストスイッチ(特にメモリ空間の切り替え)のコストが高い。
      • 例: Apache

コンテキストスイッチのコスト問題を解決する方法

  • シングルプロセス・マルチスレッドの場合
    • コンテキストスイッチのコストは改善されるが、ファイルディスクリプターを消費してしまう。
  • シングルプロセス・シングルスレッド(非同期・ノンブロッキングI/O)の場合
    • 処理すべきものがどんどんイベントキューに追加され、それらを全て1つのプロセスで捌く。
    • ファイルアクセスや通信などのCPUを使わない処理は非同期で行われ、処理が終わったらコールバック関数が呼ばれる。
      • 例: nginx、Node.js

引用:

5. Rubyでの非同期処理

時間がかかる処理は後回しにして早く応答だけしたいユースケース

  • バックグラウンドジョブの実行(非同期のキュー操作)を行う。
    • 例: メール送信、CSVアップロード。
  • Railsのデフォルトでは、(アプリケーションサーバーの?)プロセス内のスレッドでジョブを実行するため、Railsを再起動するとジョブが失われてしまう。ActionMailer#deliver_laterも同様。
  • gemの例
    • DelayedJob
    • Resque
    • Sidekiq

引用:

大量のデータを処理したいユースケース

  • parallel gemを使うと、マルチプロセスあるいはマルチスレッドでの処理が可能になる。

引用:

6. JavaScriptはシングルスレッド

  • JavaScriptはシングルスレッド(1度に1つのタスクしか実行できない)なので、非同期処理を動かせないはずだが、
    1. 優先度が高いMicro Task Queue(Promise callbackなど)と
      優先度が低いMacro Task Queue(setTimeout、setIntervalなど)
      という2種類のQueueを持ち、
    2. Event Loopに従って、
    3. Call Stackで優先度の高いQueueから処理を逐次実行する。

これにより、非同期処理っぽくなっている。

参照:

7. Goの並行処理/マルチスレッドプログラミング

goroutine

  • Goのランタイム管理を利用したスレッドコントローラ。
  • 重い処理の非同期化に使われる。
  • go キーワードを使ってマルチスレッドプログラミングを実現する。
  • 同じアドレス空間で実行されるため、共有メモリへのアクセスは必ず同期する(=スレッドセーフである)必要がある。→ channelを使う。
  • goroutineがスイッチされるタイミングは以下。
    • アンバッファなチャネルへの読み書き
    • システムコール呼び出し
    • メモリアロケーション
    • time.Sleep()が呼ばれる
    • runtime.Gosched()が呼ばれる

channel

  • 他の言語では「キュー(queue)」と呼ばれる、「First-in, First-out」(FIFO)型のデータ構造。
  • goroutineの同期を可能にする。

sync.WaitGroup

  • 複数のgoroutineの実行を待ってくれる。

今回のサンプルでは、待ち合わせにtime.Sleepを使っている箇所がいくつかあります。これは説明のためであり、本来はチャネルやsync.WaitGroupなどの「作業が完了した」ことをきちんと取り扱える仕組みを使って待ち合わせ処理を書くほうが望ましいでしょう。 さもないと、忘れた頃にコード改変でなぜか動かなくなって悩むことになったり、必要以上に待ちが発生してユニットテストの所要時間が無駄に伸びたりしてしまいます。
https://ascii.jp/elem/000/001/475/1475360/

  • Railsエンジニア的には、System SpecでjQueryの実行完了を待つために、sleep を使わずに wait_for_ajax を使うようにしていたイメージに近いかも。

引用:

8. その他参照リンク