Sign in Join
Wiki copytemporary

Parallel procedures/ja: what changed

Revision 0 to 1. Removed lines have a minus sign; added lines have a plus sign.

Title: Parallel procedures/ja

OldNewLine
1+# 概略
2+
3+このページでは、MTProcs ユニットを用いて一つの手続きを並列動作させる方法を記述します。これを用いると手続きの並列動作が簡単になり、並列処理アルゴリズムの実装が容易になります。
4+
5+並列手続き/並列メソッドは並列処理アルゴリズムにしばしば見られ、これを最初からサポートしているプログラミング言語もあります(例えば gcc の OpenMP) 。FPC にこの種の言語機能を加えるプランについては、[[OpenMP support]] をご覧下さい。言語にこの種の機能を加えると、プログラムの長さが短くなり、オーヴァヘッドが減るようなコードをコンパイラが生成できるようにできるかもしれません。一方、ある単一スレッドプログラムの中の部分的なコードを並列処理コードに変換するためには沢山の方法があります。単純なアプローチでは速度低下をもたらすことがしばしばなのです。良い結果を得るには、コンパイラが伺い知れないいくつかのパラメタを指定する必要があります。例えば、[OpenMP](http://openmp.org/wp/) および [OpenCL](http://www.khronos.org/opencl/) にある膨大な設定例と議論をご覧下さい。並列処理アルゴリズムが必要なら、MTProcs が実装の役に立ちます。
6+
7+# MTProcs を入手する
8+
9+**mtprocs.pas** ユニットは、**multithreadprocslaz.lpk** パッケージの中にあります。このパッケージは svn から得られます:
10+
11+ svn co https://lazarus-ccr.svn.sourceforge.net/svnroot/lazarus-ccr/components/multithreadprocs multithreadprocs
12+
13+例によって: IDEで一度 multithreadprocslaz.lpk を開きます。これで IDE はパッケージのパスを覚えます。プロジェクトの中でパッケージを使うには: *IDE Menu / Package / Open recent package / .../multithreadprocslaz.lpk / More / add to project* とするか、プロジェクトインスペクタから追加します。
14+
15+# 簡単な例
16+
17+短い例を出します。何も役に立つことはしませんが、並列手続きの見かけと呼出し方がわかります:
18+
19+``` pascal
20+program Test;
21+
22+{$mode objfpc}{$H+}
23+
24+uses
25+ {$IFDEF UNIX}
26+ cthreads, cmem,
27+ {$ENDIF}
28+ MTProcs;
29+
30+// 簡単な並列手続き
31+procedure DoSomethingParallel(Index: PtrInt; Data: Pointer; Item: TMultiThreadProcItem);
32+var
33+ i: Integer;
34+begin
35+ writeln(Index);
36+ for i:=1 to Index*1000000 do ; // 何かする
37+end;
38+
39+begin
40+ ProcThreadPool.DoParallel(@DoSomethingParallel,1,5,nil) // アドレス、開始する添字、終了する添字、オプションのデータ
41+end.
42+```
43+
44+出力はこんな感じになるでしょう:
45+
46+ 2
47+ 3
48+ 1
49+ 4
50+ 5
51+
52+短い注意書きをいくつか。詳細は後述します。
53+
54+- [[Multithreaded Application Tutorial/ja]] で述べられているように、UNIXでのマルチスレッディングには **cthreads** ユニットが必要です。
55+- 実行速度の面から、**cmem** ヒープマネジャの利用を勧めます。ただしこの例では差はでません。
56+- 並列手続き **DoSomethingParallel** がとる引数は個数が固定であり、予め定義されています。
57+- **Index** で、この呼出しが受け持つべき作業部分が決まります。
58+- **Data** はポインタで、*ProcThreadPool.DoParallel* の四番目の引数として与えられたものです。このパラメタはオプションであり、何に使ってもかまいませんし、利用しなくてもかまいません。
59+- **Item** はスレッドプールの持つ、より高度な特徴にアクセスするために使います。
60+- **ProcThreadPool.DoParallel** は通常の手続き呼出しのように働きます。最後まで実行したとき - つまり、全スレッドが作業を完了したら -
61+
62+リターンします。
63+
64+- この出力は、マルチスレッドに典型的な振る舞いを示します: 呼出しの順序は決まっていません。順序は実行する毎に変わり得ます。
65+
66+# 特徴
67+
68+手続きの並列実行とはこういうことです:
69+
70+- ある手続きまたはメソッドを、任意に設定した StartIndex から EndIndex までの間の Index(添字)について実行します。
71+- 一つ以上のスレッドがそれらの添字について並列に動作します。例えば Index が 1 から 10 までだとして、スレッドプールの中で利用できるスレッドが三つあったとすると、これら三つのスレッドがそれぞれ異なった添字について同時に動作します。一つのスレッドの呼出し(一つの添字に対する処理)が終わる毎に、そのスレッドには新たな添字が割り当てられ、スレッドはそれを実行します。結果はこんなふうになるでしょう: スレッド1が添字 3, 5, 7 を、スレッド2が 1, 6, 8, 9 を、スレッド3が 2, 4, 10 を。
72+- スレッド数は実行中に変化し得、最小数は保証されていません。最悪の場合、全ての添字を一つのスレッドで実行することにもなり得ます - メインスレッド自体で。
73+- スレッドの最大数は動作中のシステムに応じた適切な値に初期化されます。次のように手動で設定することもできます。
74+ ``` pascal
75+ ProcThreadPool.MaxThreadCount := 8;
76+ ```
77+- スレッドの最大数は手続き毎に設定することができます。
78+- 並列手続き/メソッドは、再帰的に並列手続き/メソッドを呼出すことが可能です。
79+- スレッドは再利用されます。つまり、各添字に対し消滅と生成を繰り返すのではなく、プールされています。2コアのプロセッサでは、プールには二つのスレッド - メインスレッドと、追加のスレッド - があり、処理全体に投入されます。
80+
81+# オーヴァヘッド、速度低下
82+
83+オーヴァヘッドは、システムに強く依存しています(コアの数と種類、共有メモリの種類、クリティカルセクションの実行速度、キャッシュの大きさ)。一般的なヒントとしては:
84+
85+- 作業の各チャンク(添字)には、少なくとも数ミリ秒かかります。
86+- オーヴァヘッドは再帰の深さとは関係がありません。
87+
88+マルチスレッディングのオーヴァヘッドは、単純にこんにちのコンピュータのアーキテクチャによるものであり、MTProcs ユニットとは無関係です:
89+
90+- スレッドを一つ生成してプログラムがマルチスレッドになった途端に、メモリマネジャはクリティカルセクションを使わざるをえなくなります。ここで実行速度が低下します。そのスレッドでなにもしなくても、プログラムの実行速度は低下するでしょう。
91+- いくつかのシステムが持つ cmem ヒープマネジャはマルチスレッドでははるかに高速です。私が Intel CPU を用いた OS X で試みたベンチマークでは、速度は10倍も違いました。
92+- 文字列とインターフェースはグローバルな参照カウンタを持っています(訳注: どこからも参照されなくなったら自動的に dispose できるように)。アクセスする毎にクリティカルセクションが必要になります。文字列を複数のスレッドで処理すると、大幅な速度低下を齎します。代わりに PChar を使ってください。
93+- 作業の各チャンク(添字)はそれぞれメモリ上の離れた部分を処理するようにすべきです。そうしないとキャッシュのアップデートが交錯します。
94+- 広過ぎるメモリ領域を扱わないでください。システムによっては、スレッド一つでメモリバスの速度を使い切ってしまいます。メモリバスが最大速度に達すると、それ以上スレッドを立ち上げても速度は向上せず、むしろ低下します。
95+
96+# 手続きに用いるスレッドの最大数を設定する
97+
98+一つの手続きに対するスレッドの最大数を、五番目の引数として指定することができます。
99+
100+``` pascal
101+begin
102+ ProcThreadPool.DoParallel(@DoSomethingParallel,1,10,nil,2); // アドレス、添字の開始値、添字の終了値、
103+ // オプション: データ(ここでは無し)、オプション: スレッドの最大数(ここでは2)
104+end.
105+```
106+
107+これは、複数のスレッドが同一のデータに対し作用するときに有用です。スレッド数が多過ぎると、キャッシュの衝突が増加し、各スレッドの実行速度が低下します。あるいは、アルゴリズムが WaitForIndex を多用する場合、実際に動作しているスレッドは少ないということもありえます。余ったスレッドは他のタスク向けにできるはずのものです。
108+
109+# 添字を待つ / 順番に実行する
110+
111+時折、ある添字の処理が、それより前の添字の処理の結果に依存する場合があります。例えば、添字 5 の計算を行い、その結果を添字 3 の結果と組み合わせて用いるような場合です。こういう時は WaitForIndex を使います:
112+
113+``` pascal
114+procedure DoSomethingParallel(Index: PtrInt; Data: Pointer; Item: TMultiThreadProcItem);
115+begin
116+ ... 添字 'Index' の処理を行う ...
117+ if Index=5 then begin
118+ if not Item.WaitForIndex(3) then exit;
119+ ... 計算処理 ...
120+ end;
121+end;
122+```
123+
124+WaitForIndex は添字を引数としてとります。その添字は、現在の添字あるいは範囲よりも小さい値です。実行が上手くいっている場合は true を返します。戻り値が false となった場合は、他のスレッドのどれかで例外が発生しています。
125+
126+更に拡張された関数 WaitForIndexRange があります。これは添字の全域について待ちます:
127+
128+``` pascal
129+if not Item.WaitForIndexRange(3,5) then exit; // 3, 4, 5を待つ
130+```
131+
132+# 例外
133+
134+あるスレッドで例外が発生すると、他のスレッドは正常に終了するでしょうが、新たな添字の処理は行われません。スレッドプールは全スレッドの実効終了を待ち、例外を惹起させます。このため、*try..except* 文を通常通り用いることができます:
135+
136+``` pascal
137+try
138+ ...
139+ ProcThreadPool.DoParallel(...);
140+ ...
141+except
142+ On E: Exception do ...
143+end;
144+```
145+
146+複数の例外が発生した場合は、最初のもののみが立ち上がります。全ての例外を処理するには、各並列手続きの中に try..except を加えてください。
147+
148+# Synchronize
149+
150+GUI の更新など、メインスレッドで関数を呼び出したい場合は、クラスメソッドの *TThread.Synchronize* を用いることができます。このメソッドは現在の TThread とメソッドのアドレスを引数としてとります。MTProc の 1.2 版から、**CurrentThread** というスレッド変数が提供されています。これを用いると synchronize の呼出しが容易になります:
151+
152+``` pascal
153+TThread.Synchronize(CurrentThread,@YourMethod);
154+```
155+
156+これで、メインイヴェント待ち行列にイヴェントをポストし、メインスレッドが指定したメソッドを実行するまで待ちます。生の FPC プログラムはイヴェント待ち行列をもたないことをよく思い出してください。イヴェント待ち行列を持つのは、LCL あるいは fpGUI プログラムです。
157+
158+TThread の子孫を自前で生成する場合は、Execute メソッドの中でこの変数に代入する必要があります。たとえば:
159+
160+``` pascal
161+procedure TYourThread.Execute;
162+begin
163+ CurrentThread:=Self;
164+ ...作業...
165+end;
166+```
167+
168+# 例: 並列ループ
169+
170+この例では、ループを並列手続きに変換する方法をステップを追って解説します。ある整数の配列 *BigArray* の最大値の計算例です。
171+
172+## オリジナルのループ
173+
174+``` pascal
175+type
176+ TArrayOfInteger = array of integer;
177+
178+function FindMaximum(BigArray: TArrayOfInteger): integer;
179+var
180+ i: PtrInt;
181+begin
182+ Result:=BigArray[0];
183+ for i:=1 to length(BigArray)-1 do begin
184+ if Result<BigArray[i] then Result:=BigArray[i];
185+ end;
186+end;
187+```
188+
189+## 作業を分割する
190+
191+作業は n 個のスレッドに均等に分配されるべきです。そこで、BigArray を大きさの等しい複数のブロックに分割し、外側のループで各ブロックを舐めるようにします。典型的には n はシステムの持つ CPU やコアの数です。MTProcs にはブロックサイズと数を決めるための便利な関数があります:
192+
193+``` pascal
194+function FindMaximum(BigArray: TArrayOfInteger): integer;
195+var
196+ BlockCount, BlockSize: PtrInt;
197+ i: PtrInt;
198+ Index: PtrInt;
199+ BlockStart, BlockEnd: PtrInt;
200+begin
201+ Result:=BigArray[0];
202+ ProcThreadPool.CalcBlockSize(length(BigArray),BlockCount,BlockSize);
203+ for Index:=0 to BlockCount-1 do begin
204+ Item.CalcBlock(Index,BlockSize,length(BigArray),BlockStart,BlockEnd);
205+ for i:=BlockStart to BlockEnd do begin
206+ if Result<BigArray[i] then Result:=BigArray[i];
207+ end;
208+ end;
209+end;
210+```
211+
212+追加の行はいかなるループにも使えます。ツールを使って自動化してもいいでしょう。これで作業は小さな部分に分割されました。ここからは各部分をより独立させなければなりません。
213+
214+訳注: 上述のように、スレッドの切り替えには時間がかかります(プロセスを起動する程ではありませんが)。そのため、
215+
216+DoParallel(なんとか, 1, 10000000, nil)
217+
218+のような呼出しは非常に不利です。スレッドの切り替えが0.1ミリ秒ですむ場合でも、何もしなくても1000秒かかります。従って各スレッドの中で十分長い処理を行うべきですし、しかも、全てのスレッドが同時に処理を終えると効率がよくなります。そのため、比較的小数の、互いに等しい大きさのブロックに分割すると有利です。
219+
220+ProcThreadPool は MTProcs ユニット内で宣言された変数です。MTProcs ユニットの initialization 節で生成・初期化されます。コンピュータの中で共通に用いられるものなので自分でインスタンス化する必要はなく、この変数をそのまま使えばいいわけです。
221+
222+この変数が持つメソッド CalcBlockSize は次のように三つの引数をとり、それぞれデータの大きさ、分割数、分割後の各ブロックの大きさです。
223+
224+procedure TProcThreadPool.CalcBlockSize(LoopLength: PtrInt; out BlockCount, BlockSize: PtrInt; MinBlockSize: PtrInt);
225+
226+コア数が 2(でハイパースレッディングのない)の機械で
227+
228+ProcThreadPool.CalcBlockSize(1000, BlockCount, BlockSize);
229+
230+のように呼び出すと、コア数を自動的に検出し、BlockCount には 2 が、BlockSize には 500 が代入されます。
231+
232+CalcBlock は、
233+
234+procedure TMultiThreadProcItem.CalcBlock(Index, BlockSize, LoopLength: PtrInt; out BlockStart, BlockEnd: PtrInt);
235+
236+と定義されていて、Index 番目のブロックが元のデータの添字のいくつからいくつまでに当るかを計算します。LoopLength、BlockSize は CalcBlockSize に渡した/から得たものを入れてください。元データの長さ (LoopLength) が BlockCount で割り切れなかった場合は、最後のブロックの長さが BlockSize より短くなりますが、CalcBlockSize はその調整も行います。CalcBlock は全ての添字が 0 から始まることを前提としています。
237+
238+## 局所変数と共有変数
239+
240+ループ内の各変数について、全てのスレッドから使える共有変数にするか、各スレッド用の局所変数にするかを決めなければなりません。共有変数である BlockCount と BlockSize は読み出すだけで変更しないので、これ以上の作業は不要です。しかし、Result のような共有変数は全てのスレッドによって書き換えられるでしょう。同期(つまりはクリティカルセクション)を用いてこれを実現することができますが、低速です。別の方法として、各スレッドがローカルにコピーを持っていて、最後にそれらの局所変数を組み合わせることもできます。
241+
242+ここでは変数 *Result* を配列にしてしまい、最後に組み合わせることにします:
243+
244+``` pascal
245+function FindMaximum(BigArray: TArrayOfInteger): integer;
246+var
247+ // shared variables
248+ BlockCount, BlockSize: PtrInt;
249+ BlockMax: PPtrInt;
250+ // local variables
251+ i: PtrInt;
252+ Index: PtrInt;
253+ BlockStart, BlockEnd: PtrInt;
254+begin
255+ ProcThreadPool.CalcBlockSize(length(BigArray),BlockCount,BlockSize);
256+ BlockMax:=AllocMem(BlockCount*SizeOf(PtrInt)); // 局所変数用を割り当てるための空間を用意する
257+ // 各ブロックの最大値を求める
258+ for Index:=0 to BlockCount-1 do begin
259+ // compute maximum of block
260+ Item.CalcBlock(Index,BlockSize,length(BigArray),BlockStart,BlockEnd);
261+ BlockMax[Index]:=BigArray[BlockStart];
262+ for i:=BlockStart to BlockEnd do begin
263+ if BlockMax[Index]<BigArray[i] then BlockMax[Index]:=BigArray[i];
264+ end;
265+ end;
266+ // ブロック間の最大値を求める
267+ // (何百ものスレッドがある場合は、もっとよい解決法がある)
268+ Result:=BlockMax[0];
269+ for Index:=1 to BlockCount-1 do
270+ Result:=Max(Result,BlockMax[Index]);
271+
272+ FreeMem(BlockMax);
273+end;
274+```
275+
276+このアプローチは単刀直入であり、自動化が可能ですが、それでもなおプログラマの関与がある程度必要です。
277+
278+## DoParallel
279+
280+最後のステップは内側のループを手続き内手続きにもちこんで、ループを DoParallelLocalProc 呼出しで置き換えることです。
281+
282+**注意**: FPC はいまもなお局所手続き型を定義することを認めていません。そのため、危険なタイプキャストを用いる必要があります。
283+
284+``` pascal
285+function TMainForm.FindMaximum(BigArray: TArrayOfInteger): integer;
286+var
287+ BlockCount, BlockSize: PtrInt;
288+ BlockMax: PPtrInt;
289+
290+ procedure FindMaximumParallel(Index: PtrInt; Data: Pointer;
291+ Item: TMultiThreadProcItem);
292+ var
293+ i: integer;
294+ BlockStart, BlockEnd: PtrInt;
295+ begin
296+ // ブロック内最大値を求める
297+ Item.CalcBlock(Index,BlockSize,length(BigArray),BlockStart,BlockEnd);
298+ BlockMax[Index]:=BigArray[BlockStart];
299+ for i:=BlockStart to BlockEnd do
300+ if BlockMax[Index]<BigArray[i] then BlockMax[Index]:=BigArray[i];
301+ end;
302+var
303+ Index: PtrInt;
304+begin
305+ // 作業を等しい大きさのブロックに分割する
306+ ProcThreadPool.CalcBlockSize(length(BigArray),BlockCount,BlockSize);
307+ // 局所/スレッド変数を割り当てる
308+ BlockMax:=AllocMem(BlockCount*SizeOf(PtrInt));
309+ // 各ブロックについて最大値を求める
310+ ProcThreadPool.DoParallelLocalProc(@FindMaximumParallel,0,BlockCount-1);// 注意: 安全ではないタイプキャスト
311+ // 全ブロックの最大値を求める
312+ Result:=BlockMax[0];
313+ for Index:=1 to BlockCount-1 do
314+ Result:=Max(Result,BlockMax[Index]);
315+ FreeMem(BlockMax);
316+end;
317+```
318+
319+ほとんどがコピペ作業であり、これまた自動化が可能でしょう。
320+
321+# 例: 並列ソート
322+
323+ユニット MTPutils には **ParallelSortFPList** 関数があります。これは MTProcs を用いて TFPList を並列にソートします。比較用の関数を与える必要があります。
324+
325+``` pascal
326+procedure ParallelSortFPList(List: TFPList; const Compare: TListSortCompare;
327+ MaxThreadCount: integer = 0; const OnSortPart: TSortPartEvent = nil);
328+```
329+
330+この関数は並列マージソートのアルゴリズムを用いています。引数 MaxThreadCount は DoParallel に渡されます。0 はシステム既定値を用いることを意味します。
331+
332+オプションとして、各スレッドでソートを行うための自前のソート用関数 (OnSortPart) を与えることができます。例えば、各ブロックをクイックソートして、それらをマージすることもできます。そうすれば、**並列クイックソート**を得ることになります。クイックソートの実装例は TFPList.Sort をご覧下さい。
333+
334+# 実験的
335+
336+1.0.1以降、入れ子になった手続きを呼び出せる新たな DoParallelLocalProc が用意されました。コンパイラはいまだに入れ子になった手続きの呼出しをサポートしていないので、危険なタイプキャストが必要なことに注意してください。例えば:
337+
338+``` pascal
339+procedure DoSomething(Value: PtrInt);
340+var
341+ p: array[1..2] of Pointer;
342+
343+ procedure SubProc(Index: PtrInt; Data: Pointer; Item: TMultiThreadProcItem);
344+ begin
345+ p[Index]:=Pointer(Value); // 局所変数と引数へのアクセスが可能に!
346+ end;
347+
348+var
349+ i: Integer;
350+begin
351+ ProcThreadPool.DoParallelLocalProc(@SubProc,1,2); // 注意: SubProcの型チェックは行われない
352+ // DoSomething は手続き。メソッドは不可。
353+end;
354+```
355+
356+これで沢山のリファクタリングが不要となり、並列手続きの可読性が上がります。
357+
358+# 関連項目
359+
360+- [[Multithreaded Application Tutorial/ja]]
361+- [[OpenCL]]
362+
363+---
364+*From the Free Pascal wiki, page "Parallel procedures/ja", as it stood on 2020-02-23 after a change by Trev, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*

Back to history