
JapanTaxiではソフトウェアエンジニアの未経験者採用を行っており、 その方々を対象に日々メンタリングを実施したり、不定期で勉強会を開催しています。 今回は、プロセスとスレッドについての勉強会を行いましたので、その内容を紹介したいと思います。
経緯
先日開催されたメドピアさんとの合同勉強会(JapanTaxi x MedPeer Ruby/Rails勉強会)で、メドピアの村上さんによる、UnicornでActionCableを使おうとしてハマりかけたことの話の中で、WebサーバーであるUnicornとPumaの違いについてプロセスベースとスレッドベースという説明がされており、これについて社内で勉強会を開催する事になりました。
勉強会の実施方法
ハンズオン形式でサンプルコードを使用してプロセスとスレッドの違いについて学習することにしました。前半は、座学で一般的に言われているプロセスとスレッドの違いについて学習し、 後半で、サンプルコードを使用して実際にどのような動作をするのか確認します。
座学
まずは一般的にプロセスとスレッドの違いで説明されている事を確認しました。カーネルスレッドやライトウェイトプロセス等を考慮してないため正確な説明ではありませんが割愛させていただきます。
プロセス
- OSから見える処理の単位
- 1プロセス1CPUの関係
- メモリ空間を共有していない
- 他のプロセスのメモリにはアクセスできない
- プロセス間の同期が難しい
- (スレッドと比較して)1プロセス毎の情報量が多い
- 情報量が多いので切り替えが遅い
- (マルチスレッドプログラムと比較して)メモリ効率が低い
- 仮想アドレスと物理アドレスの解決が高コストのため切り替えが遅い
- (マルチスレッドプログラムと比較して)大量のメモリが使用できる
- 他のプロセスのメモリにはアクセスできない
- マルチプロセッサの場合、(一般的に)シングルプロセスマルチスレッドプログラムよりもマルチプロセスプログラムの方が高速
スレッド
- プロセスの中で並列的に処理を行う仕組み
- メモリ空間を共有している
- 非同期に実行できる処理がある場合は、(一般的に)シングルスレッドプログラムよりもマルチスレッドプログラムの方が高速
演習
マルチプロセス、マルチスレッドプログラミングのサンプルコードを用いて、下記の内容を確認します。
目的
- メモリ空間を共有する、しないとは
- OSからどのように見えるのか
- メモリ効率の比較
- 生成速度の比較
- 最大生成可能数の比較
環境
$ 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をインストールしてください。
topコマンドを使えば定期的に更新されますが、Macのtopでthreadを表示する方法が不明だったためpsを使用しています。
$ brew install watch
メモリ空間を共有する、しないとは
マルチプロセス
プログラムを実行すると、下記の結果が出力されます。
$ ./multi_process_sample.out parent n[0x7ffee36e2954]=1 child n[0x7ffee36e2954]=1 child n[0x7ffee36e2954]=2 parent n[0x7ffee36e2954]=2
このプログラムが何を行っているかを説明します。プログラム中でSleepを行っていますが、これはプロセスの状態を確認しやすくするためです。
- プログラムを起動
- この時点で親プロセスが生成されます
- 変数nを
1で初期化 - 10秒Sleepプロセスをfork
- この時点で子プロセスが生成されます
- (子プロセス)変数nのアドレスと値を出力
- (子プロセス)10秒Sleep
- (子プロセス)変数nを
+1 - (子プロセス)変数nのアドレスと値を出力
- (子プロセス)終了
- (親プロセス)変数nのアドレスと値を出力
- (親プロセス)子プロセスの終了を待ち合わせる
- (親プロセス)変数nを
+1 - (親プロセス)変数nのアドレスと値を出力
- (親プロセス)終了
出力結果を説明します。 parentが親プロセスの出力、childが子プロセスの出力です
- (親プロセス)変数nのアドレスと値を出力
- 変数nは
1で初期化されているのでn=1です
- 変数nは
- (子プロセス)変数nのアドレスと値を出力
- (子プロセス)変数nを
+1 - (子プロセス)変数nのアドレスと値を出力
- 変数nを
+1したため、値が2になっています
- 変数nを
- (親プロセス)変数nを
+1 - (親プロセス)変数nのアドレスと値を出力
- 子プロセスで変数nは
2になりましたが、親プロセスでも値は2になっています- この事により親プロセスと子プロセスでメモリ空間が共有されていない事が確認できました
- 子プロセスで変数nは
マルチスレッド
プログラムを実行すると、下記の結果が出力されます。
$ ./multi_thread_sample.out main n[0x7ffee483e964]=1 sub n[0x7ffee483e964]=1 sub n[0x7ffee483e964]=2 main n[0x7ffee483e964]=3
このプログラムが何を行っているかを説明します。マルチプロセスとほぼ等価の処理です。
- プログラムを起動
- この時点でメインスレッドが生成されます
- 通常プロセスを起動するとスレッドが一つだけ起動します
- これをメインスレッドと呼びます
- 通常プロセスを起動するとスレッドが一つだけ起動します
- この時点でメインスレッドが生成されます
- 変数nを
1で初期化 - 10秒Sleep
- スレッドを生成
- この時点でサブスレッドが生成されます
- (サブスレッド)変数nのアドレスと値を出力
- (サブスレッド)10秒Sleep
- (サブスレッド)変数nを
+1 - (サブスレッド)変数nのアドレスと値を出力
- (サブスレッド)終了
- (メインスレッド)変数nのアドレスと値を出力
- (メインスレッド)サブスレッドの終了を待ち合わせる
- (メインスレッド)変数nを
+1 - (メインスレッド)変数nのアドレスと値を出力
- (メインスレッド)終了
出力結果を説明します。 mainがメインスレッドの出力、subがサブスレッドの出力です
- (メインスレッド)変数nのアドレスと値を出力
- 変数nは
1で初期化されているのでn=1です
- 変数nは
- (サブスレッド)変数nのアドレスと値を出力
- (サブスレッド)変数nを
+1 - (サブスレッド)変数nのアドレスと値を出力
- 変数nを
+1したため、値が2になっています
- 変数nを
- (メインスレッド)変数nを
+1 - (メインスレッド)変数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,544 | multi_process_sample |
| 1,724 | multi_thread_sample |
生成速度の比較
multi_process_benchmark.outとmulti_thread_benchmark.outを実行し、生成時間を比較してみてください。 このプログラムは標準出力を行うだけのプロセス、もしくはスレッドを100個生成し、全てが終了するまでの時間を計測しています。 筆者の環境では下記の結果になりました。 この事から、スレッドのほうが生成速度が速いことがわかります。
実際のプログラムでは、プロセスやスレッドを都度生成する事は稀です。 高コスト、メモリリーク、生成できない可能性がある等の問題があるからです。 起動時に必要数を確保しプログラム終了時まで破棄しないスレッドプール等の仕組みが使われます。
| 処理時間(μs) | プログラム |
|---|---|
| 19,081 | multi_process_benchmark |
| 8,389 | multi_thread_benchmark |
最大生成可能数の比較
multi_process_challenge.outとmulti_thread_challenge.outを実行し、生成時間を比較してみてください。 このプログラムは標準出力を行うだけのプロセス、もしくはスレッドの生成を失敗するまで繰り返します。 筆者の環境では下記の結果になりました。この事から、スレッドのほうが同時に大量に生成できることがわかります。 最大数はOSが決めています。 プロセッサ数以上は同時に処理できないため、単純に数が多いほうが速いということではありません。
| 同時生成数 | プログラム |
|---|---|
| 4,095 | multi_thread_challenge |
| 1,193 | multi_process_challenge |
振り返り
今回は、プロセスとスレッドの勉強会でしたが、メモリと深い関係があるところだったため、 ついメモリやレジスタに話が脱線しがちになったので、機会があればメモリとレジスタの勉強会を開催したいと思います。 (勉強会中つい説明に熱が入り、lldbを使ったライブデバッグにまで発展してしまいました。)