Teigha Multithreading High-Level API (Part 1 of 3)

Andrew Markovich

December 14, 2017

Introduction

Teigha Kernel provides a simple and powerful high-level cross-platform API called Thread Pool Services which is used for invoking multithreading functionality inside Teigha-based applications and libraries. Thread Pool Services is provided by Teigha Kernel as a separate extension module that can be loaded on demand. It provides a set of interfaces for working with threads, events, multi-thread queues and so on.

This article is part of a series of articles about the Thread Pool Services interface.

Loading the ThreadPool.tx module

All high-level multithreading interfaces are declared in a single header file. To invoke them, application source code must include this header file:

#include "RxThreadPoolService.h"

Thread Pool Services is implemented as a separate Teigha extension module. An application can load this module to access the main OdRxThreadPoolService module interface:

// Load thread pool module
OdRxThreadPoolServicePtr pThreadPool = ::odrxDynamicLinker()->loadApp(OdThreadPoolModuleName);
if (pThreadPool.isNull())
  throw OdError(eNullPtr); // ThreadPool.tx not found.

Statically linked applications must additionally link with the ThreadPool.lib static library (ThreadPool.a for non-Windows platforms). Also, statically linked applications must register the ThreadPool module in the static modules map:

/************************************************************************/
/* Define a module map for statically linked modules                    */
/************************************************************************/
#if !defined(_TOOLKIT_IN_DLL_) || defined(__MWERKS__)

ODRX_DECLARE_STATIC_MODULE_ENTRY_POINT(OdRxThreadPoolService);

ODRX_BEGIN_STATIC_MODULE_MAP()
  ODRX_DEFINE_STATIC_APPLICATION(OdThreadPoolModuleName, OdRxThreadPoolService)
ODRX_END_STATIC_MODULE_MAP()

#endif

After loading the ThreadPool.tx module, the accessed OdRxThreadPoolService module interface can be invoked by the application for running multi-threaded operations.

Using Thread Pool Services

We can illustrate the use of Thread Pool Services in a real application using two different tasks:

  1. Load and render a database inside multiple threads.
  2. Process a raster image using multiple threads.

Similar or approximate tasks can be applied to different client applications.

Loading and rendering a database using multiple threads

Loading and rendering different databases in multiple threads is a typical task for multithreading applications. This example is useful for implementing your own application that invokes multithreading functionality for optimized processing of a large number of drawings.

Prerequisites

We will use the following simple function to render a database to a raster image:

// Simple function to render database into raster image
static OdGiRasterImagePtr renderDbToImage(OdDbDatabase *pDb, const OdChar *pRenderDevice, long picWidth, long picHeight)
{
  // Create vectorization context
  OdGiContextForDbDatabasePtr pDbCtx = OdGiContextForDbDatabase::createObject();
  // Create rendering device
  OdGsModulePtr pGsModule = ::odrxDynamicLinker()->loadModule(pRenderDevice);
  OdGsDevicePtr pDevice = pGsModule->createBitmapDevice();
  pDbCtx->setDatabase(pDb);
  // Initialize rendering device
  pDevice = OdDbGsManager::setupActiveLayoutViews(pDevice, pDbCtx);
  pDevice->setLogicalPalette(::odcmAcadDarkPalette(), 256);
  pDevice->setBackgroundColor(ODRGB(0, 0, 0));
  pDbCtx->setPaletteBackground(ODRGB(0, 0, 0));
  // Setup size of output contents
  pDevice->onSize(OdGsDCRect(OdGsDCPoint(0, picHeight), OdGsDCPoint(picWidth, 0)));
  // Zoom into model
  OdAbstractViewPEPtr(pDevice->viewAt(0))->zoomExtents(pDevice->viewAt(0));
  // Render
  pDevice->update();
  // Create clone of rendered raster image to correctly release all vectorizer resources in current thread
  OdGiRasterImagePtr pRaster = OdGiRasterImageHolder::createObject(OdGiRasterImagePtr(pDevice->properties()->getAt(OD_T("RasterImage"))));
  // Return rendered raster image
  return pRaster;
}

Before running database rendering in multiple threads, we can check that our renderDbToImage function works correctly, and that the vectorization module is loaded and accessible. Also, a preliminary call of rendering to an empty database will allocate static module resources, so running in multiple threads will be safer after that:

{ // Check that rendering device can be normally loaded and works correctly. Additionally apply
  // rendering modules static data initialization.
  OdDbDatabasePtr pEmptyDb = svcs.createDatabase();
  ::renderDbToImage(pEmptyDb, OdWinOpenGLModuleName, 1024, 1024);
}

Constructing a multithreading queue

We can get the required number of threads from the OdRxThreadPoolService interface directly, but this solution will complicate coding. Thread Pool Services provides multithread queues that simplify working with multiple threads. First we need to create a multithreading queue:

OdApcQueuePtr pMTQueue = pThreadPool->newMTQueue(ThreadsCounter::kMtLoadingAttributes | ThreadsCounter::kMtRegenAttributes);

This is done using a single call to the OdRxThreadPoolService interface. Since we will invoke database loading and rendering inside multiple threads, we pass some additional flags in the newMTQueue method; these flags work as hints for inter per-thread cache and buffer allocation. The following table describes all flag values:

ThreadsCounter::kNoAttributes

Can be set for simple multithreading processes that don’t require any special initializations.

ThreadsCounter::kMtLoadingAttributes

Must be set for processes that use loading of multiple databases in multiple threads.

ThreadsCounter::kMtRegenAttributes

Must be set for processes that invoke parallel threads during database regeneration.

ThreadsCounter::kStRegenAttributes

Must be set for processes that invoke vectorization in multiple threads.

ThreadsCounter::kMtDisplayAttributes

Must be set for processes that invoke database displaying in multiple threads.

ThreadsCounter::kMtModelerAttributes

Must be set for processes that invoke modeling operations in parallel threads.

ThreadsCounter::kAllAttributes

Enable all attributes.

Running multiple threads in a queue

First (for simple example code) we use an additional structure in which we will store rendering parameters that are actual for each running thread. A pointer to this structure will be passed into each running thread using a thread function argument.

// Helper structure with settings equal for all run threads
struct RenderDbToImageContext
{
  const OdChar *m_pRenderDevice;
  long m_picWidth, m_picHeight;
  OdDbHostAppServices *m_pServices;
  void setup(const OdChar *pRenderDevice, long picWidth, long picHeight, OdDbHostAppServices *pServices)
  { m_pRenderDevice = pRenderDevice; m_picWidth = picWidth; m_picHeight = picHeight; m_pServices = pServices; }
};

Now we can construct and fill these structure data members:

// Create "render database to image" context shareable between threads
RenderDbToImageContext renderDbContext;
renderDbContext.setup(OdWinOpenGLModuleName, 1024, 1024, &svcs);

Implement a class inherited from the OdApcAtom interface. In our example, objects of this type will be different for each thread (this simplifies passing per-thread data), but for simpler processes we can use one OdApcAtom-based object for all threads.

// Thread running method implementation
class RenderDbToImageCaller : public OdApcAtom
{
  OdString m_inputFile;
  OdGiRasterImagePtr *m_pOutputImage;
  public:
    RenderDbToImageCaller *setup(OdString inputFile, OdGiRasterImagePtr *pOutputImage)
    { m_inputFile = inputFile; m_pOutputImage = pOutputImage;
      return this; }
    virtual void apcEntryPoint(OdApcParamType pMessage)
    {
      RenderDbToImageContext *pContext = (RenderDbToImageContext*)pMessage;
      OdDbDatabasePtr pDb = pContext->m_pServices->readFile(m_inputFile);
      if (pDb.isNull())
        return;
      *m_pOutputImage = ::renderDbToImage(pDb, pContext->m_pRenderDevice, pContext->m_picWidth, pContext->m_picHeight);
    }
};

Our example RenderDbToImageCaller class contains only a single OdApcAtom interface: the apcEntryPoint method. This overridden method will be called for each thread. Since we passed a RenderDbToImageContext structure pointer as a thread argument, we can simply convert the pMessage argument into this data type. Implementation of the apcEntryPoint method simply loads a database from a specified input file and renders it to a raster image using the previously described renderDbToImage function.

At this point we are ready to run a separate processing thread for each input database file:

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

Here we construct our RenderDbToImageCaller object for each thread, set up its parameters and pass it to the OdApcQueue::addEntryPoint method. As the second argument of the OdApcQueue::addEntryPoint method, we pass a pointer to our RenderDbToImageContext structure. The queue will immediately run processing in threads after an OdApcQueue::addEntryPoint method call; no additional actions are required.

Completing running threads

Because we will wait for the result of multithreading processing before more steps, we must be sure that all threads have completed their jobs. This task can be done using a single method call:

// Wait for threads completion
pMTQueue->wait();

The OdApcQueue::wait method waits until all threads are completed and returns execution to the caller. After this method call, we can be sure that all threads completed their tasks and all output data is available for further processing.

The next article will describe processing raster images using multiple threads.