[c#] ConcurrentDictionaryを作って学ぶマルチスレッドデザインパターンの記事です。
プログラムを実行するとは何か
スレッドを考えることが自然であることを理解してもらうためには、まずプログラムを実行するということを理解する必要があるよ。
ここでは慣れ親しんだ以下のようなプログラムを考えよう。
# include <stdio.h>
int sum(int x,int y){
return x+y;
}
int main(){
int x = sum(5,6);
printf("Hello Single Thread World");
}
ここでC#ではなくてCを持ち出したのは、C#はプログラムを実行する部分が抽象化されて処理系に依存しない形になってしまっているので、今回の内容を理解するにはすこし不適かなと思ったからなのでご了承を。c#も最終的にはアセンブリ言語と同等のバイナリとして実行するので問題はないっちゃ。
詳しく知りたい方は"c# IL"とか"c# 中間言語"とかで検索するといいかも。(c#とVisualBasicでライブラリが共有できるのはこれのおかげなのさ。便利だね。)
仕切り直して、このCのコードをアセンブリにコンパイルしてみよう。アセンブリはgccをつかえば出せるぜ。
アセンブリっていうのは処理系が電子回路レベルで扱える命令だけでつくられたプログラミング言語(ただし処理系依存)で、x86-64とかで検索すると色々出てくるよ。(今回はintel記法で表記されたアセンブリを考えるよ。アセンブリの本質はそれが機械命令と一対一で対応していることなのでいろんな記法があるのさ!)
それがっ
ドン!
sum:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx
pop rbp
ret
.LC0:
.string "Hello Single Thread World"
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov esi, 6
mov edi, 5
call sum
mov DWORD PTR [rbp-4], eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
leave
ret
これは読めないよ。なれないと。。
でも大丈夫、今回の記事の目的ではこれを理解する必要は全くなくて、
大切なことは***call
とかjmp
とかのgoto命令とtagでプログラムの動きが記述されていることだよ。
処理系はmainタグの先頭から実行を初めて、上述のジャンプ命令で別のタグ(メモリ番地に対応する)に飛びながら実行を進めていくのさ。
つまり、処理系の実行経路はタグからタグをつないでいった一本の向き付きの曲線であらわせるのさ!
だから、こういうのをシングルスレッド***(糸、線の英訳)と呼ぶのさ。
お待ちかねのマルチスレッドについて
ここからはマルチスレッドとは何かに入っていくよ。
シングルスレッドの様子がわかった人はマルチスレッドの様子も多少想像できるかもしれないね。
ここでは具体的なソースコードを見せて、コンパイルすると、すごく複雑になるから、概念的に説明します。
イメージはこれ
これがマルチスレッドなのはある実行経路中でclone
or fork
が行われていることからわかると思う。
では、clone
やfork
というのは何なのかという話についてで、これらはosがもっているAPIなんだよ!
というのも、プログラムが実行される手順をより低レベルから見ると(もっと詳しい人からみたらいろいろ端折りすぎだよって怒られるかもだけど、、)
1. プログラムが処理系のアーキテクチャ依存のバイナリに変換される。(コンパイル、リンク)
2. バイナリがosによって割り当てられたメモリ空間にロードされる。(ローダ)
3. osによって割り当てられたメモリ空間のスタックにmain関数(or その他のエントリ関数)の先頭メモリ番地をpushする。
4. osによって割り当てられたCPUのスレッド(これは物理的な回路の機能単位を意味してる。3コア4スレッドとかは聞いたことあるだろう?)はメモリ空間のスタックからpopしてきたメモリ番地から実行をスタートする。
というふるまいをしていて、
fork命令やclone命令は(os依存なので名前は少し違うかもだけど)荒っぽく言うとこの実行の手順の3番目からを引数で指定した関数の番地について行えという命令なんだ1。詳しくは"システムコールを経由する生のLinuxスレッド"を参照してみてほしい。
つまり、スレッドは処理系の動作レベルから出てきている概念で、実行の一つの単位を与えてくれるものなんだ!
イメージをつかんでもらったところで厳密な定義(一般に受け入れられたものではないが)を与えるとすれば
スレッドとは
エントリポイントとメモリ上に自身専用のスタック領域を持っている。
そのエントリポイントから実行を始めることによる実行単位を与える。
メインスレッド(メインプロセス)とは
osがセットした実行のエントリポイントから始まるスレッド。
メモリ空間がosから割り当てられており、その中で実行を行う。
サブスレッドとは
メインスレッドからある関数を指定して生成されたスレッド。
メインスレッドとメモリ空間を共有しており、その中で実行を行う。
サブプロセスとは
メインスレッドからある関数を指定して生成されたスレッド。
osからメインスレッドとは共通部分を持たないメモリ空間が与えられ、その中で実行を行う。
他のスレッドorプロセスとは共有のメモリ領域を通じてやり取りをする。
以上!
次の記事
-
大体はこれであってると思うけど、実はもっと引数は多くていろんなことができるし、上で行った手順は高機能なosが存在する場合のスレッドについてであり、もっと貧弱な環境だと、waitを使ってスレッドを切り替えながら実行していくというマルチスレッドの実現方法もあるっぽい ↩