マンデルブロプロジェクトの設計
この章の例題は、マンデルブロフラクタルのマルチスレッド計算です。ユーザはフラクタルを見て、そのウィンドウでパンやズームをすることができます。
コードに飛び込む前に、フラクタルとその計算をどのように実現しようとしているのかを広く理解しておく必要があります。
マンデルブロ・フラクタルは、複素数(a + bi)で動作する数値集合です。各ピクセルは繰り返し計算された値に関連付けられています。もしこの繰り返しの値が無限大に向かって発散するならば、その画素はマンデルブロー集合から外れていることになります。そうでなければ、その画素はマンデルブロー集合の中にあることになります。マンデルブロフラクタルの視覚的表現は次のようになります。
この画像の黒い画素はそれぞれ無限の値に発散する傾向がありますが、白い画素は有限の値に拘束されています。白い画素はマンデルブロ集合に属します。
マルチスレッドの観点から興味深いのは,その画素がマンデルブロー集合に属するかどうかを判断するためには、その画素の発散の有無を推定するための式を反復しなければならないということです。反復の回数が多ければ多いほど、「そうだ、この画素はマンデルブロー集合の中にある、白い画素だ」と主張するのはより安全です。
さらに楽しいことに、 グラフィカルなプロットの中の任意の値を取り、 常にマンデルブロの公式を適用して、 画素が黒か白かを推論することができます。その結果、フラクタルのグラフィックの中を無限にズームすることができます。主な制限は2つだけです。
- お使いのCPUのパワーが画像生成速度を阻害します。
- CPUアーキテクチャの浮動小数点数の精度がズームを制限しています。ズームを続けると、スケールファクタが15から17の有効数字しか扱えないため、視覚的なアーチファクトが発生します。
アプリケーションのアーキテクチャは慎重に設計する必要があります。スレッドを使って作業をしているので、デッドロックを起こしたり、スレッドを枯渇させたり、さらに悪いことにUIをフリーズさせたりすることは非常に簡単です。
私たちは本当にCPUを最大限に使いたいと思っています。そのためには、各コアでできるだけ多くのスレッドを実行します。各スレッドは、結果を返す前にマンデルブロ集合の一部を計算します。
アプリケーションのアーキテクチャは以下の通りです。
アプリケーションは3つの部分に分かれています。
- MandelbrotWidget: これは、表示する絵を要求します。描画とユーザーのインタラクションを処理します。このオブジェクトは UI スレッドに存在します。
- MandelbrotCalculator: これはピクチャのリクエストを処理し、結果として得られたJobResultsを集約してからMandelbrotWidgetに送り返します。このオブジェクトは自分のスレッドに常駐します。
- Job: これは、結果をMandelbrotCalculatorに転送する前に、最終的な画像の一部を計算します。各ジョブは独自のスレッドで管理されています。
MandelbrotCalculator スレッドは QThreadPool クラスを使用して、自分のスレッドでジョブをディスパッチします。これは、CPUコアに応じて完全にスケーリングされます。各ジョブは、最終的な画像の一行分を計算してから、JobResultオブジェクトを使ってMandelbrotCalculatorに送り返します。
MandelbrotCalculator スレッドは、実際には計算のオーケストレーターです。計算が完了する前に画像をズームインするユーザーを考えてみましょう。MandelbrotWidgetはMandelbrotCalculatorに新しいピクチャを要求し、MandelbrotCalculatorは新しいジョブをディスパッチする前に現在のジョブをすべてキャンセルしなければなりません。
このプロジェクトに最後の制約を加えます。ミューテックスは非常に便利なツールですが、スレッドがお互いを待たなければならず、エラーが発生しやすいものです。これを実現するために、Qtが提供する複数の概念や技術に頼ることにします: マルチスレッドシグナル/スロット、暗黙の共有などです。
スレッド間の共有状態を最小限にすることで、できるだけ高速に実行させることができるようになります。そのためにここにいるのは、CPUコアを燃やすためですよね?
これで大まかなイメージが明確になったので、実装を開始します。ch09-mandelbrot-threadpoolという名前のQt Widget Applicationプロジェクトを新規作成します。.proファイルにCONFIG += c++14を忘れずに追加してください。