MoTLab -GO Inc. Engineering Blog-MoTLab -GO Inc. Engineering Blog-

プロセスとスレッドについての勉強会

行灯Labo低レイヤ
May 25, 2018


💁🏻
※本記事は Mobility Technologies の前身である JapanTaxi 時代に公開していたもので、記事中での会社やサービスに関する記述は公開当時のものです。
An image from Notion

JapanTaxiではソフトウェアエンジニアの未経験者採用を行っており、 その方々を対象に日々メンタリングを実施したり、不定期で勉強会を開催しています。 今回は、プロセスとスレッドについての勉強会を行いましたので、その内容を紹介したいと思います。

経緯

先日開催されたメドピアさんとの合同勉強会(JapanTaxi x MedPeer Ruby/Rails勉強会)で、メドピアの村上さんによる、UnicornでActionCableを使おうとしてハマりかけたことの話の中で、WebサーバーであるUnicornとPumaの違いについてプロセスベースとスレッドベースという説明がされており、これについて社内で勉強会を開催する事になりました。

勉強会の実施方法

ハンズオン形式でサンプルコードを使用してプロセスとスレッドの違いについて学習することにしました。前半は、座学で一般的に言われているプロセスとスレッドの違いについて学習し、 後半で、サンプルコードを使用して実際にどのような動作をするのか確認します。

座学

まずは一般的にプロセスとスレッドの違いで説明されている事を確認しました。カーネルスレッドやライトウェイトプロセス等を考慮してないため正確な説明ではありませんが割愛させていただきます。

プロセス

  • OSから見える処理の単位
  • 1プロセス1CPUの関係
  • メモリ空間を共有していない
    • 他のプロセスのメモリにはアクセスできない
      • プロセス間の同期が難しい
    • (スレッドと比較して)1プロセス毎の情報量が多い
      • 情報量が多いので切り替えが遅い
      • (マルチスレッドプログラムと比較して)メモリ効率が低い
    • 仮想アドレスと物理アドレスの解決が高コストのため切り替えが遅い
    • (マルチスレッドプログラムと比較して)大量のメモリが使用できる
  • マルチプロセッサの場合、(一般的に)シングルプロセスマルチスレッドプログラムよりもマルチプロセスプログラムの方が高速

スレッド

  • プロセスの中で並列的に処理を行う仕組み
  • メモリ空間を共有している
    • 他のスレッドのメモリにアクセスできる
      • スレッド間の同期が容易
    • スレッドセーフな実装にする必要がある
      • メモリを共有しているので、他のスレッドに予期せぬ影響を与えてしまうことがある
    • (プロセスと比較して)1スレッド毎に必要な情報量が少ない
      • 情報量が少ないので切り替えが早い
      • (マルチプロセスプログラミングと比較して)メモリ効率が高い
    • マルチプロセスプログラミングと比較して)大量のメモリ確保が行えない
  • 非同期に実行できる処理がある場合は、(一般的に)シングルスレッドプログラムよりもマルチスレッドプログラムの方が高速

演習

マルチプロセス、マルチスレッドプログラミングのサンプルコードを用いて、下記の内容を確認します。

目的

  • メモリ空間を共有する、しないとは
  • OSからどのように見えるのか
  • メモリ効率の比較
  • 生成速度の比較
  • 最大生成可能数の比較

環境

  • macOS 10.12.6
  • 筆者の独断により、C言語を使用します。
  • サンプルコードはGitHubに公開しています。
  • コンパイラにはclangを使用して動作確認を行っています。
    • Macユーザーの場合は、Command Line Tools for Xcodeをインストールしてください。
$ clang -v
Apple LLVM version 9.0.0 (clang-900.0.39.2)
Target: x86_64-apple-darwin16.7.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
$ git clone https://github.com/JapanTaxi/mpmt
$ cd mpmt
$ make all

プロセスの状態を確認するのにpsコマンドを使用しますが、定期的に更新したいのでwatchをインストールしてください。

$ brew install watch

メモリ空間を共有する、しないとは

マルチプロセス

プログラムを実行すると、下記の結果が出力されます。

$ ./multi_process_sample.out
parent n[0x7ffee36e2954]=1
child  n[0x7ffee36e2954]=1
child  n[0x7ffee36e2954]=2
parent n[0x7ffee36e2954]=2

このプログラムが何を行っているかを説明します。プログラム中でSleepを行っていますが、これはプロセスの状態を確認しやすくするためです。

  1. プログラムを起動
    • この時点で親プロセスが生成されます
  2. 変数nを1で初期化
  3. 10秒Sleepプロセスをfork
    • この時点で子プロセスが生成されます
  4. (子プロセス)変数nのアドレスと値を出力
  5. (子プロセス)10秒Sleep
  6. (子プロセス)変数nを+1
  7. (子プロセス)変数nのアドレスと値を出力
  8. (子プロセス)終了
  9. (親プロセス)変数nのアドレスと値を出力
  10. (親プロセス)子プロセスの終了を待ち合わせる
  11. (親プロセス)変数nを+1
  12. (親プロセス)変数nのアドレスと値を出力
  13. (親プロセス)終了

出力結果を説明します。 parentが親プロセスの出力、childが子プロセスの出力です

  1. (親プロセス)変数nのアドレスと値を出力
    • 変数nは1で初期化されているのでn=1です
  2. (子プロセス)変数nのアドレスと値を出力
    • forkシステムコールを実行すると、プロセス(メモリ情報も全て)がコピーされます
      • 変数nは1で初期化されているのでn=1です
    • 変数nのアドレスが親プロセスと子プロセスで一致している事に注目してください
      • 同じアドレスを指していますが、実際には異なるメモリ上に値が存在します
        • 仮想メモリという仕組みで物理メモリを隠蔽しているためこのような結果になります
  3. (子プロセス)変数nを+1
  4. (子プロセス)変数nのアドレスと値を出力
    • 変数nを+1したため、値が2になっています
  5. (親プロセス)変数nを+1
  6. (親プロセス)変数nのアドレスと値を出力
    • 子プロセスで変数nは2になりましたが、親プロセスでも値は2になっています
      • この事により親プロセスと子プロセスでメモリ空間が共有されていない事が確認できました

マルチスレッド

プログラムを実行すると、下記の結果が出力されます。

$ ./multi_thread_sample.out
main n[0x7ffee483e964]=1
sub  n[0x7ffee483e964]=1
sub  n[0x7ffee483e964]=2
main n[0x7ffee483e964]=3

このプログラムが何を行っているかを説明します。マルチプロセスとほぼ等価の処理です。

  1. プログラムを起動
    • この時点でメインスレッドが生成されます
      • 通常プロセスを起動するとスレッドが一つだけ起動します
        • これをメインスレッドと呼びます
  2. 変数nを1で初期化
  3. 10秒Sleep
  4. スレッドを生成
    • この時点でサブスレッドが生成されます
  5. (サブスレッド)変数nのアドレスと値を出力
  6. (サブスレッド)10秒Sleep
  7. (サブスレッド)変数nを+1
  8. (サブスレッド)変数nのアドレスと値を出力
  9. (サブスレッド)終了
  10. (メインスレッド)変数nのアドレスと値を出力
  11. (メインスレッド)サブスレッドの終了を待ち合わせる
  12. (メインスレッド)変数nを+1
  13. (メインスレッド)変数nのアドレスと値を出力
  14. (メインスレッド)終了

出力結果を説明します。 mainがメインスレッドの出力、subがサブスレッドの出力です

  1. (メインスレッド)変数nのアドレスと値を出力
    • 変数nは1で初期化されているのでn=1です
  2. (サブスレッド)変数nのアドレスと値を出力
  3. (サブスレッド)変数nを+1
  4. (サブスレッド)変数nのアドレスと値を出力
    • 変数nを+1したため、値が2になっています
  5. (メインスレッド)変数nを+1
  6. (メインスレッド)変数nのアドレスと値を出力
    • サブスレッドでも変数nを+1しているため、値が3になっています
      • この事によりメインスレッドとサブスレッドででメモリ空間が共有されている事が確認できました

OSからどのように見えるのか

マルチプロセス

Terminalを2つ起動しmulti_process_sample.outを実行しつつ、下記のコマンドを実行してください。

$ watch -n 1 ps cMo ppid,vsz,rss

注目していただきたいのは、COMMAND(コマンド名)、PID(プロセスID)、PPID(親プロセスID)、RSS(物理メモリサイズ)です。 まず、プログラム起動直後はmulti_process_sampleが一つしか確認できないと思います。 この時点ではプロセスが一つしか存在しないためです。

USER              PID   TT   %CPU STAT PRI     STIME     UTIME COMMAND  PPID    RSS
yoshimitsudaiki 27912 s004    0.0 S    31T   0:00.00   0:00.00 multi_p 17990   1712

その後(10秒Sleep解除後)、プロセスが二つ確認できると思います。 二つ目に作成されたmulti_process_sampleのPPID(27912)が、最初に作成されたmulti_process_sampleのPIDになっていると思います。 これは一つ目のプロセスから二つ目のプロセスが生成されたという事を意味しています。 このようにPPIDをたどる事によって、プロセスの親子関係を確認することができます。

USER              PID   TT   %CPU STAT PRI     STIME     UTIME COMMAND  PPID    RSS
yoshimitsudaiki 27912 s004    0.0 S    31T   0:00.00   0:00.00 multi_p 17990   1712
yoshimitsudaiki 27931 s004    0.0 S    31T   0:00.00   0:00.00 multi_p 27912    832

最後にRSSですが、これは物理的に使用しているメモリのサイズです。 二つのプロセスが起動することによって1,712KB+832KBのメモリが使用されている事が確認できます。

マルチスレッド

Terminalを2つ起動しmulti_thread_sample.outを実行しつつ、下記のコマンドを実行してください。

$ watch -n 1 ps cMo ppid,vsz,rss

注目していただきたいのは、COMMAND(コマンド名)、PID(プロセスID)、RSS(物理メモリサイズ)です。 まず、プログラム起動直後はmulti_thread_sampleと同じPIDを持つプロセスは一つしか確認できないと思います。 この時点ではスレッドが一つしか存在しないためです。

USER              PID   TT   %CPU STAT PRI     STIME     UTIME COMMAND  PPID    RSS
yoshimitsudaiki 35216 s004    0.0 S    31T   0:00.00   0:00.00 multi_t 17990   1724

その後(10秒Sleep解除後)、同一のプロセス(PID:35216)が二つ確認できると思います。 これは一つ目のプロセス中に二つのスレッドが存在している事を意味しています。 本来であればtid(スレッドID)という概念で識別できるのですが、Macで確認する方法が不明だったため割愛させていただきます。

USER              PID   TT   %CPU STAT PRI     STIME     UTIME COMMAND  PPID    RSS
yoshimitsudaiki 35216 s004    0.0 S    31T   0:00.00   0:00.00 multi_t 17990   1724
                35216         0.0 S    31T   0:00.00   0:00.00         17990   1724

RSSはプロセス毎に共有されているため、1724KBのメモリが使用されている事が確認できます。

メモリ効率の比較

multi_process_sample.outとmulti_thread_sample.outを実行し、 psコマンドでRSS(物理メモリサイズ)を比較してみてください。 筆者の環境では下記の結果になりました。 この事から、マルチスレッドプログラムのほうがメモリ効率が良いことがわかります。

RSS(KB)プログラム
2,544multi_process_sample
1,724multi_thread_sample

生成速度の比較

multi_process_benchmark.outとmulti_thread_benchmark.outを実行し、生成時間を比較してみてください。 このプログラムは標準出力を行うだけのプロセス、もしくはスレッドを100個生成し、全てが終了するまでの時間を計測しています。 筆者の環境では下記の結果になりました。 この事から、スレッドのほうが生成速度が速いことがわかります。

実際のプログラムでは、プロセスやスレッドを都度生成する事は稀です。 高コスト、メモリリーク、生成できない可能性がある等の問題があるからです。 起動時に必要数を確保しプログラム終了時まで破棄しないスレッドプール等の仕組みが使われます。

処理時間(µs)プログラム
19,081multi_process_benchmark
8,389multi_thread_benchmark

最大生成可能数の比較

multi_process_challenge.outとmulti_thread_challenge.outを実行し、生成時間を比較してみてください。 このプログラムは標準出力を行うだけのプロセス、もしくはスレッドの生成を失敗するまで繰り返します。 筆者の環境では下記の結果になりました。この事から、スレッドのほうが同時に大量に生成できることがわかります。 最大数はOSが決めています。 プロセッサ数以上は同時に処理できないため、単純に数が多いほうが速いということではありません。

同時生成数プログラム
4,095multi_thread_challenge
1,193multi_process_challenge

振り返り

今回は、プロセスとスレッドの勉強会でしたが、メモリと深い関係があるところだったため、 ついメモリやレジスタに話が脱線しがちになったので、機会があればメモリとレジスタの勉強会を開催したいと思います。 (勉強会中つい説明に熱が入り、lldbを使ったライブデバッグにまで発展してしまいました。)


JapanTaxiでは、全国タクシー開発部エンジニアとして一緒に働く仲間を募集しています。まずは一度話を聞きに来てみませんか?


💁🏻
※本記事は Mobility Technologies の前身である JapanTaxi 時代に公開していたもので、記事中での会社やサービスに関する記述は公開当時のものです。

Mobility Technologies では共に日本のモビリティを進化させていくエンジニアを募集しています。話を聞いてみたいという方は、是非 募集ページ からご相談ください!