Teigha Multithreading Low-Level API (Part 1 of 4)

Andrew Markovich
January 25, 2018

Tags: performance Example getting started

In previous blog articles we described Teigha’s high-level multithreading API (Teigha Thread Pool Services), and how using it, Teigha-based applications can invoke multithreading operations with a minimal number of calls and helper data structures. But some client applications and products have their own multithreading mechanisms or invoke third-party multithreading libraries. For example, applications can run threads using APIs from the companies that develop QT, Boost, and MFC. Products written in .NET languages can run threads using built-in .NET framework objects. Moreover some kinds of applications execute Teigha functionality inside their own threads (for example, multithread exports, thumbnail creation plugins, drawing converters, etc.). This series of articles describes how to safely use third-party multithreading functionality together with Teigha.

Even if your application does not invoke third-party multithreading functionality and works only with the Teigha high-level multithreading API, this series of articles can still be helpful, for example, future articles in the series about using mutex objects and atomic operations.

Note that this series about the low-level API contains examples that are based on the source code examples from previous articles about the Teigha Multithreading High-Level API.

Using Teigha functionality inside external threads

An application that uses Teigha inside its own threads must inform the Teigha libraries that they will be run in a multithreaded environment. In this case, Teigha Kernel provides the Threads Counter singleton object.

The Threads Counter singleton object provides a low-level interface to work with the Teigha libraries together with external threads. It provides functions to register and access external threads information and to manage communication between external threads and the Teigha libraries.

To use the Threads Counter object, an application must include the following header file:

#include "ThreadsCounter.h"

The Threads Counter object is implemented as a singleton, which means that this object is represented as a single object instance that is alive during the entire Teigha libraries lifecycle. The odThreadsCounter() function can be used to access it any time:

FIRSTDLL_EXPORT ThreadsCounter&   odThreadsCounter();

Before using Teigha functionality inside external threads, the application must register the threads using the ThreadsCounter::increase method:

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

Each thread on any operating system has its own unique identifier (described in a future article in this series). The application can pass one or more thread IDs using the ThreadsCounter::increase method to register them on the Teigha side, initialize required per-thread caches, and so on. Note that the thread attributes (the third method argument) were described previously in Teigha Multithreading High-Level API in the “Constructing a multithreading queue” section. The Threads Counter has a similar meaning to Threads Pool Services. When working with the Teigha libraries from an external thread is completed, the application must call the ThreadsCounter::decrease method to free allocated per-thread caches and decrease the registered number of threads:

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

Since each thread can have its own allocated resources on the Teigha side, the thread that invokes the Teigha libraries on the application side must call the ThreadsCounter::startThread method within the thread execution function body before using Teigha functionality:

void startThread();

After using Teigha functionality within the thread execution function body, it must call the ThreadsCounter::stopThread method:

void stopThread();

An application can use the ThreadsCounter::hasThread method to check whether the thread ID is registered already on the Teigha side:

bool hasThread(unsigned nThreadId, unsigned *pThreadAttributes);

Example of using the Threads Counter

To illustrate using the Threads Counter object methods, we will extend the example from the Teigha Multithreading High-Level API blog article.

Prerequisites

First, to simplify our example, we can add our own thread managing functionality. In real applications this functionality typically is much more complex. Many applications invoke third-party multithreading functionality from other libraries such as QT, Boost, and so on.

For this example we will invoke pure Microsoft® Windows® API functions to implement two helper classes:

  • SimpleWinThread wraps a single thread within our threads manager.
  • SimpleWinThreadsPool implements storage for working threads and provides functions to run and wait for completion of multiple threads.
// Simple thread object based on Windows API threads
class SimpleWinThread
{
  public:
    typedef OdSharedPtr Ptr;
    typedef OdArray 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 > 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();
      }
    }
};

In our simple example, the SimpleWinThread class calls the ThreadsCounter::increase method in its constructor to register this thread ID on the Teigha side and calls the ThreadsCounter::decrease method in its destructor to inform the Teigha libraries that this thread will not invoke Teigha functionality inside it during the nearest time. The SimpleWinThreadsPool class stores all working threads inside the m_lockedThreads array and all SimpleWinThread class destructions will be called when this array is cleared in the SimpleWinThreadsPool::wait method.

Construct an instance of the SimpleWinThreadsPool object in the main example function for future use:

// Create simple windows threads manager
SimpleWinThreadsPool winThreadPool;

Loading and rendering databases in multiple threads

Modify the RenderDbToImageCaller class from the previous Teigha Multithreading High-Level API example to use SimpleWinThreadsPool instead of Teigha Thread Pool Services:

// 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;
    }
};

Now all thread jobs will be done inside the RenderDbToImageCaller::entryPoint static method, which first calls the ThreadsCounter::startThread method to allocate all required per-thread caches on the Teigha side and calls the ThreadsCounter::stopThread method at the end to deallocate them. The RenderDbToImageCaller::run method is used to start per-thread operations.

Modify our main example application function to run threads using the SimpleWinThreadsPool class instead of Thread Pool Services OdApcQueue:

// 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::createObject()->
      setup(inputFileName, &generatedRasters[nInput], &renderDbContext)->
        run(winThreadPool));
}

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

The lockedObjects array is used in our example to prevent RenderDbToImageCaller objects deletion before all multithread operations are completed. As before, we construct the RenderDbToImageCaller object, set it up, and run processing for it inside the thread.

Finally we call the SimpleWinThreadsPool::wait method to wait for completion of all started threads and finalize them.

Processing raster images inside multiple threads

Similar to the RenderDbToImageCaller class, modify the ProcessImageCaller class that was used to initiate per-thread raster image processing:

// Thread running method implementation
class ProcessImageCaller : public OdRxObject
{
  OdSmartPtr 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;
    }
};

And modify the main example application function:

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

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

After all described modifications, the example that previously invoked the Thread Pool Services API can be started completely using its own thread implementation and generate similar output images for input drawings with (near) similar performance measurements.

Watch for an article coming soon about working with built-in Teigha mutex objects for a continuation of using the multithreading low-level API.

All posts