この記事は、マルチスレッドに使用されるスレッドプールサービスインターフェースに関する一連の記事の一部です。前の記事については、パート1を参照してください。
複数のスレッドを使用したラスター画像の処理
これは、マルチスレッドを使用した最適化に適したもう1つの典型的なタスクです。ラスター画像は何十億ものピクセルで構成されており、それぞれを処理でき、ピクセルごとの計算が非常に複雑になることもあります。最良のケースは、個々のピクセルを処理しても他のピクセルに影響を与えない場合です。つまり、スレッドは追加の同期(ミューテックス、イベントなど)なしで同時に実行できます。スレッドが独自のデータヒープのみを操作し、他の処理スレッドに影響を与えないタスクは、マルチスレッド最適化に最適な場所です。
前提条件
ラスター画像処理にマルチスレッドを使用する例として、前のステップ(複数のデータベースのロードとレンダリング)で生成されたすべての画像から、単一の最終画像を生成します。このタスクのために、ラスター画像ラッパー(ラスター画像ラッパーの詳細については、「Teighaラスター画像ラッパーを使用した画像処理(2016年11月)」の記事を参照)を作成できます。これは、scanLinesメソッド呼び出し中に、要求されたスキャンライン内の各ピクセルの平均色を計算します。
// Raster image generator (combine input images into single image)
class GeneratedRasterImage : public OdGiRasterImageWrapper
{
protected:
OdGiRasterImagePtrArray m_inputImages;
public:
virtual const OdUInt8* scanLines() const { return NULL; }
virtual void scanLines(OdUInt8* scnLines, OdUInt32 firstScanline, OdUInt32 numLines = 1) const
{
OdUInt32 scanLen = scanLineSize(), pixWidth = pixelWidth() * 3;
OdArray<OdUInt8Array> inputScanlines;
inputScanlines.resize(m_inputImages.size());
for (OdUInt32 i = firstScanline; i < firstScanline + numLines; i++)
{
for (OdUInt32 nImage = 0; nImage < inputScanlines.size(); nImage++)
{
inputScanlines[nImage].resize(scanLen);
m_inputImages[nImage]->scanLines(inputScanlines[nImage].asArrayPtr(), i);
}
OdUInt8 *pScanLine = scnLines + ((i - firstScanline) * scanLen);
for (OdUInt32 nPixel = 0; nPixel < pixWidth; nPixel += 3)
{
double clrMerge[3] = { 0.0, 0.0, 0.0 };
for (OdUInt32 nImage = 0; nImage < inputScanlines.size(); nImage++)
{
clrMerge[0] += inputScanlines[nImage][nPixel + 0];
clrMerge[1] += inputScanlines[nImage][nPixel + 1];
clrMerge[2] += inputScanlines[nImage][nPixel + 2];
}
clrMerge[0] /= inputScanlines.size();
clrMerge[1] /= inputScanlines.size();
clrMerge[2] /= inputScanlines.size();
pScanLine[nPixel + 0] = (OdUInt8)clrMerge[0];
pScanLine[nPixel + 1] = (OdUInt8)clrMerge[1];
pScanLine[nPixel + 2] = (OdUInt8)clrMerge[2];
}
}
}
GeneratedRasterImage() {}
void configureImage(const OdGiRasterImage *pOriginal, OdGiRasterImagePtrArray &inputImages)
{
setOriginal(pOriginal);
m_inputImages = inputImages;
}
};
GeneratedRasterImageクラスは要求されたピクセルをスキャンライン内でオンデマンドで計算するため、追加のラスター画像ラッパーが必要です。これは、画像処理の結果を単純に保持し、その後の画像保存中に余分な計算なしでそれを返します。
// Container for processed raster image
class ProcessedRasterImage : public OdGiRasterImageWrapper
{
OdUInt8Array m_processedPixels;
public:
virtual const OdUInt8* scanLines() const { return m_processedPixels.getPtr(); }
virtual void scanLines(OdUInt8* scnLines, OdUInt32 firstScanline, OdUInt32 numLines = 1) const
{
const OdUInt8 *pixData = m_processedPixels.getPtr();
const OdUInt32 scnSize = scanLineSize();
::memcpy(scnLines, pixData + scnSize * firstScanline, numLines * scnSize);
}
void process(OdUInt32 firstScanline, OdUInt32 numLines = 1)
{
original()->scanLines(m_processedPixels.asArrayPtr() + scanLineSize() * firstScanline, firstScanline, numLines);
}
ProcessedRasterImage() {}
void configureImage(const OdGiRasterImage *pOriginal)
{
setOriginal(pOriginal);
m_processedPixels.resize(scanLineSize() * pixelHeight());
}
};
もちろん、より高度なアプリケーションでは、GeneratedRasterImageクラスとProcessedRasterImageクラスを1つのクラスに統合し、事前に計算された結果を内部に保持し、scanLinesメソッドが呼び出されるたびに再計算しないようにすることができます。しかし、この変更は以下の例を複雑にするため、より分かりやすくするために両方のクラスをスキップしました。
ProcessedRasterImage::process呼び出しは、GeneratedRasterImage::scanLinesメソッドを呼び出して、要求されたスキャンライン内のピクセルを計算し、その結果を内部配列に格納します。各スレッドは、独自のスキャンラインのセットを計算します。
これで、GeneratedRasterImageクラスとProcessedRasterImageクラスの両方を構築して設定できます。
// Create final raster image generator
OdSmartPtr<GeneratedRasterImage> pGenImage = OdRxObjectImpl<GeneratedRasterImage>::createObject();
pGenImage->configureImage(generatedRasters[0], generatedRasters);
// Create container for processed final raster image
OdSmartPtr<ProcessedRasterImage> pProcImage = OdRxObjectImpl<ProcessedRasterImage>::createObject();
pProcImage->configureImage(pGenImage);
複数のスレッドで処理を実行する
前回の記事と同様に、まずマルチスレッドタスク用のキューを構築します。
pMTQueue = pThreadPool->newMTQueue(ThreadsCounter::kNoAttributes, 4, kMtQueueAllowExecByMain);
newMTQueueメソッドの引数が変更されました。
- データベースのロードやベクトル化のような複雑なプロセスを呼び出さない単純なタスクであるため、フラグを設定する代わりに、最初の引数にThreadsCounter::kNoAttributesを渡します。ThreadsCounter::kNoAttributesは、追加のスレッド初期化や初期化解除が不要であることを意味します。
- 2番目の引数として4を渡します。これは、タスクに4つのスレッドが必要であることを正確に把握しており、この知識がスレッド割り当てを最適化することを意味します。さらに、これによりタスク実行に正確に4つのスレッドが使用されることが保証されます。スレッドプールに十分な空きスレッドがない場合、それらを生成します。
- 3番目の引数に、追加のkMtQueueAllowExecByMainフラグを渡します。このフラグは、マルチスレッドキューにメインプロセススレッドを補助スレッドの1つとして使用するように指示します。データベースのロードやベクトル化プロセスを呼び出すような複雑なタスクの実行には、このフラグを使用することはお勧めできません。なぜなら、それらはメインプロセススレッドのレベルでいくつかのサブタスクの実行を必要とする可能性があるからです。しかし、ラスター画像処理のような単純なタスクの場合、マルチスレッドキューは3つの補助スレッドしか割り当てることができず、最後のサブタスクのためにすでに利用可能なメインプロセススレッドを呼び出すことができるため、このフラグの使用は正当化されます。そうしないと、メインプロセススレッドはすべてのスレッドの実行が終了するまで単に待機することになります。
これで、ラスター画像処理のために4つのスレッドを実行できます。
// Run threads for raster image processing
const OdUInt32 nScanlinesPerThread = pProcImage->pixelHeight() / 4;
for (OdUInt32 nThread = 0; nThread < 4; nThread++)
{
OdUInt32 nScanlinesPerThisThread = nScanlinesPerThread;
if (nThread == 3) // Height can be not dividable by 2, so last thread can have onto one scanline less.
nScanlinesPerThisThread = pProcImage->pixelHeight() - nScanlinesPerThread * 3;
pMTQueue->addEntryPoint(OdRxObjectImpl<ProcessImageCaller>::createObject()->setup(pProcImage, nScanlinesPerThread * nThread, nScanlinesPerThisThread), (OdApcParamType)NULL);
}
このコード部分は、前回の記事のデータベースのロードとレンダリングのコードと非常によく似ています。違いは、最後のaddEntryPointメソッド引数を使用してスレッドにデータを渡す必要がないことです。したがって、単にNULLを渡します。各スレッドはスキャンラインの一部を処理します。最後のスレッド(画像の高さによって異なります)は、他のスレッドよりも1つ少ないスキャンラインを処理する場合があります。
スレッドの完了を待機し、マルチスレッドキューを解放します。これはもう必要ありません。
// Wait threads completion
pMTQueue->wait();
pMTQueue.release();
最後に、生成された出力ラスター画像を保存します(ラスター画像の保存の詳細については、「Teighaラスター画像ラッパーを使用した画像処理(2016年11月)」の記事を参照してください)。
// Save output image
OdRxRasterServicesPtr pRasSvcs = odrxDynamicLinker()->loadApp(RX_RASTER_SERVICES_APPNAME, false);
if (pRasSvcs.isNull()) // Check that raster services module correctly loaded
throw OdError(eNullPtr);
bool bSaveState = pRasSvcs->saveRasterImage(pProcImage, outputFileName);
if (!bSaveState)
throw OdError(eFileWriteError);
テスト用に、4つの入力図面があります。
1024本の線を持つ図面 |
|
1024個の円を持つ図面 |
|
|
|
|
1024個の長方形ポリラインを持つ図面 |
|
1024個の長方形ソリッドを持つ図面 |
最終的に生成されたラスター画像(4つの入力画像を結合した結果)は次のようになります。
4つの入力ラスター画像を最終的なラスター画像に結合した結果
次の記事では、マルチスレッド使用時のパフォーマンス測定について説明します。