Teighaマルチスレッド低レベルAPI(4部構成の第1部)

以前のブログ記事では、TeighaのハイレベルマルチスレッドAPI(Teighaスレッドプールサービス)と、それを使用してTeighaベースのアプリケーションが最小限の呼び出しとヘルパーデータ構造でマルチスレッド操作を呼び出す方法について説明しました。しかし、一部のクライアントアプリケーションや製品は独自のマルチスレッドメカニズムを持っているか、サードパーティのマルチスレッドライブラリを呼び出します。たとえば、アプリケーションはQT、Boost、MFCを開発する企業のAPIを使用してスレッドを実行できます。.NET言語で書かれた製品は、組み込みの.NETフレームワークオブジェクトを使用してスレッドを実行できます。さらに、一部の種類のアプリケーションは、独自の内部スレッドでTeighaの機能を実行します(たとえば、マルチスレッドエクスポート、サムネイル作成プラグイン、図面コンバーターなど)。この一連の記事では、サードパーティのマルチスレッド機能をTeighaと安全に併用する方法について説明します。

アプリケーションがサードパーティのマルチスレッド機能を呼び出さず、TeighaのハイレベルマルチスレッドAPIのみを使用する場合でも、この一連の記事は、たとえば、ミューテックスオブジェクトとアトミック操作の使用に関する今後の記事などで役立つ可能性があります。

この低レベルAPIに関するシリーズには、TeighaマルチスレッドハイレベルAPIに関する以前の記事のソースコード例に基づいた例が含まれていることに注意してください。

外部スレッド内でのTeigha機能の使用

独自の内部スレッド内でTeighaを使用するアプリケーションは、Teighaライブラリがマルチスレッド環境で実行されることをTeighaライブラリに通知する必要があります。この場合、Teigha KernelはThreads Counterシングルトンオブジェクトを提供します。

Threads Counterシングルトンオブジェクトは、外部スレッドとともにTeighaライブラリを操作するための低レベルインターフェースを提供します。これは、外部スレッド情報を登録およびアクセスし、外部スレッドとTeighaライブラリ間の通信を管理する機能を提供します。

Threads Counterオブジェクトを使用するには、アプリケーションは次のヘッダーファイルを含める必要があります。

#include "ThreadsCounter.h"

Threads Counterオブジェクトはシングルトンとして実装されており、これはこのオブジェクトがTeighaライブラリのライフサイクル全体にわたって存続する単一のオブジェクトインスタンスとして表現されることを意味します。odThreadsCounter()関数を使用して、いつでもそれにアクセスできます。

FIRSTDLL_EXPORT ThreadsCounter&   odThreadsCounter();

外部スレッド内でTeigha機能を使用する前に、アプリケーションはThreadsCounter::increaseメソッドを使用してスレッドを登録する必要があります。

void increase(unsigned nThreads, const unsigned* aThreads, unsigned nThreadAttributes = ThreadsCounter::kNoAttributes);

どのオペレーティングシステム上の各スレッドも、独自のユニークな識別子を持っています(このシリーズの今後の記事で説明されます)。アプリケーションは、ThreadsCounter::increaseメソッドを使用して1つ以上のスレッドIDを渡し、Teigha側でそれらを登録し、必要なスレッドごとのキャッシュを初期化するなどを行うことができます。スレッド属性(3番目のメソッド引数)は、以前にTeighaマルチスレッドハイレベルAPIの「マルチスレッドキューの構築」セクションで説明されています。Threads CounterはThreads Pool Servicesと同様の意味を持ちます。外部スレッドからのTeighaライブラリとの作業が完了したら、アプリケーションはThreadsCounter::decreaseメソッドを呼び出して、割り当てられたスレッドごとのキャッシュを解放し、登録されたスレッド数を減らす必要があります。

void decrease(unsigned nThreads, const unsigned* aThreads);

各スレッドはTeigha側で独自の割り当てられたリソースを持つことができるため、アプリケーション側でTeighaライブラリを呼び出すスレッドは、Teigha機能を使用する前に、スレッド実行関数本体内でThreadsCounter::startThreadメソッドを呼び出す必要があります。

void startThread();

スレッド実行関数本体内でTeigha機能を使用した後は、ThreadsCounter::stopThreadメソッドを呼び出す必要があります。

void stopThread();

アプリケーションはThreadsCounter::hasThreadメソッドを使用して、スレッドIDがTeigha側で既に登録されているかどうかを確認できます。

bool hasThread(unsigned nThreadId, unsigned *pThreadAttributes);

スレッドカウンターの使用例

スレッドカウンターオブジェクトメソッドの使用を説明するために、Teighaマルチスレッド高レベルAPIブログ記事の例を拡張します。

前提条件

まず、例を簡素化するために、独自のスレッド管理機能を追加できます。実際のアプリケーションでは、この機能は通常はるかに複雑です。多くのアプリケーションは、QT、Boostなどの他のライブラリからサードパーティのマルチスレッド機能を呼び出します。

この例では、純粋なMicrosoft® Windows® API関数を呼び出して、2つのヘルパークラスを実装します。

  • SimpleWinThreadは、スレッドマネージャー内で単一のスレッドをラップします。
  • SimpleWinThreadsPoolは、ワーキングスレッドのストレージを実装し、複数のスレッドを実行して完了を待つ機能を提供します。
// Simple thread object based on Windows API threads
class SimpleWinThread
{
  public:
    typedef OdSharedPtr<SimpleWinThread> Ptr;
    typedef OdArray<Ptr> PtrsArray;
  protected:
    HANDLE m_hThread;
    DWORD m_threadId;
  public:
    SimpleWinThread(LPTHREAD_START_ROUTINE runFcn, void *fcnArg, PtrsArray &attachPtr, OdUInt32 threadAttributes)
    {
      m_hThread = ::CreateThread(NULL, 0, runFcn, fcnArg, CREATE_SUSPENDED, &m_threadId);
      ::SetThreadPriority(m_hThread, THREAD_PRIORITY_ABOVE_NORMAL);
      ::odThreadsCounter().increase(1, (unsigned int*)&m_threadId, threadAttributes);
      attachPtr.push_back(Ptr(this));
      ::ResumeThread(m_hThread);
    }
    ~SimpleWinThread() { ::odThreadsCounter().decrease(1, (unsigned int*)&m_threadId); }
    HANDLE threadHandle() const { return m_hThread; }
};

// Simple threads pool for SimpleWinThread thread objects
class SimpleWinThreadsPool
{
  OdArray<HANDLE, OdMemoryAllocator<HANDLE> > m_runningThreads;
  SimpleWinThread::PtrsArray m_lockedThreads;
  public:
    SimpleWinThreadsPool() {}
    ~SimpleWinThreadsPool() { wait(); }

    void runNewThread(LPTHREAD_START_ROUTINE runFcn, void *fcnArg, OdUInt32 threadAttributes)
    {
      m_runningThreads.push_back((new SimpleWinThread(runFcn, fcnArg, m_lockedThreads, threadAttributes))->threadHandle());
    }
    void wait()
    {
      if (!m_runningThreads.isEmpty())
      {
        ::WaitForMultipleObjects(m_runningThreads.size(), m_runningThreads.getPtr(), TRUE, INFINITE);
        m_runningThreads.clear();
        m_lockedThreads.clear();
      }
    }
};

この簡単な例では、SimpleWinThreadクラスはコンストラクタでThreadsCounter::increaseメソッドを呼び出してTeigha側でこのスレッドIDを登録し、デストラクタでThreadsCounter::decreaseメソッドを呼び出して、このスレッドが近い将来Teigha機能を呼び出さないことをTeighaライブラリに通知します。SimpleWinThreadsPoolクラスは、すべてのワーキングスレッドをm_lockedThreads配列内に格納し、SimpleWinThreadsPool::waitメソッドでこの配列がクリアされると、すべてのSimpleWinThreadクラスのデストラクタが呼び出されます。

将来の使用のために、メインのサンプル関数でSimpleWinThreadsPoolオブジェクトのインスタンスを構築します。

// Create simple windows threads manager
SimpleWinThreadsPool winThreadPool;

複数のスレッドでのデータベースのロードとレンダリング

以前のTeighaマルチスレッド高レベルAPIの例からRenderDbToImageCallerクラスを修正し、TeighaスレッドプールサービスではなくSimpleWinThreadsPoolを使用するようにします。

// Thread running method implementation
class RenderDbToImageCaller : public OdRxObject
{
  OdString m_inputFile;
  OdGiRasterImagePtr *m_pOutputImage;
  RenderDbToImageContext *m_pThreadCtx;
  public:
    RenderDbToImageCaller *setup(OdString inputFile, OdGiRasterImagePtr *pOutputImage, RenderDbToImageContext *pThreadCtx)
    { m_inputFile = inputFile; m_pOutputImage = pOutputImage; m_pThreadCtx = pThreadCtx;
      return this; }
    static DWORD WINAPI entryPoint(LPVOID pArg)
    {
      ::odThreadsCounter().startThread();
      RenderDbToImageCaller *pCaller = (RenderDbToImageCaller*)pArg;
      OdDbDatabasePtr pDb = pCaller->m_pThreadCtx->m_pServices->readFile(pCaller->m_inputFile);
      if (pDb.isNull())
      {
        ::odThreadsCounter().stopThread();
        return EXIT_FAILURE;
      }
      *(pCaller->m_pOutputImage) = ::renderDbToImage(pDb, pCaller->m_pThreadCtx->m_pRenderDevice, pCaller->m_pThreadCtx->m_picWidth, pCaller->m_pThreadCtx->m_picHeight);
      ::odThreadsCounter().stopThread();
      return EXIT_SUCCESS;
    }
    RenderDbToImageCaller *run(SimpleWinThreadsPool &threadPool)
    {
      threadPool.runNewThread(entryPoint, this, ThreadsCounter::kMtLoadingAttributes | ThreadsCounter::kMtRegenAttributes);
      return this;
    }
};

これで、すべてのスレッドジョブはRenderDbToImageCaller::entryPoint静的メソッド内で実行されます。このメソッドは、最初にThreadsCounter::startThreadメソッドを呼び出してTeigha側で必要なスレッドごとのキャッシュをすべて割り当て、最後にThreadsCounter::stopThreadメソッドを呼び出してそれらを解放します。RenderDbToImageCaller::runメソッドは、スレッドごとの操作を開始するために使用されます。

メインのサンプルアプリケーション関数を修正し、スレッドプールサービスOdApcQueueの代わりにSimpleWinThreadsPoolクラスを使用してスレッドを実行するようにします。

// Locked per-thread data structures
OdRxObjectPtrArray lockedObjects;

// Run loading and rendering process
for (OdUInt32 nInput = 0; nInput < generatedRasters.size(); nInput++)
{
  OdString inputFileName(argv[2 + nInput]);
  lockedObjects.push_back(
    OdRxObjectImpl<RenderDbToImageCaller>::createObject()->
      setup(inputFileName, &generatedRasters[nInput], &renderDbContext)->
        run(winThreadPool));
}

// Wait threads completion
winThreadPool.wait();
lockedObjects.clear();

lockedObjects配列は、すべてのマルチスレッド操作が完了する前にRenderDbToImageCallerオブジェクトが削除されるのを防ぐために、この例で使用されています。以前と同様に、RenderDbToImageCallerオブジェクトを構築し、設定し、スレッド内でその処理を実行します。

最後に、SimpleWinThreadsPool::waitメソッドを呼び出して、開始されたすべてのスレッドの完了を待ち、それらを終了します。

複数のスレッド内でのラスター画像の処理

RenderDbToImageCallerクラスと同様に、スレッドごとのラスター画像処理を開始するために使用されたProcessImageCallerクラスを修正します。

// Thread running method implementation
class ProcessImageCaller : public OdRxObject
{
  OdSmartPtr<ProcessedRasterImage> m_pProcImage;
  OdUInt32 m_scanLineFrom, m_nScanLines;
  public:
    ProcessImageCaller *setup(ProcessedRasterImage *pProcImage, OdUInt32 scanLineFrom, OdUInt32 nScanLines)
    { m_pProcImage = pProcImage; m_scanLineFrom = scanLineFrom; m_nScanLines = nScanLines;
      return this; }
    static DWORD WINAPI entryPoint(LPVOID pArg)
    {
      ::odThreadsCounter().startThread();
      ProcessImageCaller *pCaller = (ProcessImageCaller*)pArg;
      pCaller->m_pProcImage->process(pCaller->m_scanLineFrom, pCaller->m_nScanLines);
      ::odThreadsCounter().stopThread();
      return EXIT_SUCCESS;
    }
    ProcessImageCaller *run(SimpleWinThreadsPool &threadPool)
    {
      threadPool.runNewThread(entryPoint, this, ThreadsCounter::kNoAttributes);
      return this;
    }
};

そして、メインのサンプルアプリケーション関数を修正します。

// 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;
  lockedObjects.push_back(
    OdRxObjectImpl<ProcessImageCaller>::createObject()->
      setup(pProcImage, nScanlinesPerThread * nThread, nScanlinesPerThisThread)->
        run(winThreadPool));
}

// Wait threads completion
winThreadPool.wait();
lockedObjects.clear();

上記すべての修正後、以前はスレッドプールサービスAPIを呼び出していたサンプルは、独自の独自のスレッド実装を使用して完全に開始でき、入力図面に対して(ほぼ)同様のパフォーマンス測定値で同様の出力画像を生成できます。

マルチスレッド低レベルAPIの使用の続きとして、Teighaの組み込みミューテックスオブジェクトの操作に関する記事が近日公開されますので、ご期待ください。

今すぐ始める

ODAソフトウェアを60日間無料でお試しください。
リスクなし、クレジットカード不要。

無料で試す