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

Andrew Markovich

January 09, 2018

This article is part of a series of articles about the Thread Pool Services interface used for multithreading. For the previous article, see Part1 and Part2.

Measuring performance

For performance measurements we include a Teigha Kernel performance timer:

#include "OdPerfTimer.h"

And construct it in our main application function:
// Init performance timer
OdPerfTimerWrapper perfTimer;

Here we’ve used the OdPerfTimerWrapper performance timer wrapper. Usage of the wrapper simplifies our code, since it will automatically construct a performance timer for us in the OdPerfTimerWrapper class constructor and destruct it in the OdPerfTimerWrapper class destructor at the end of our main executable function.

Now we can wrap the part of the source code that requires performance measurements using OdPerfTimerBase start and stop methods:

// Start timing for "render database to image" process
perfTimer.getTimer()->start();

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

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

// Output timing for "render database to image" process
perfTimer.getTimer()->stop();
odPrintConsoleString(L"%u files loaded and rendered in %f seconds\n", generatedRasters.size(), perfTimer.getTimer()->countedSec());

The start and stop method scopes include thread execution and waiting for the thread execution result. Finally we output the measurement results in seconds. Similarly place the OdPerfTimerBase start and stop method scopes to measure performance of raster image processing. Results of measurements look like this:

4 files loaded and rendered in 0.509886 seconds

Final raster image processed in 0.542890 seconds

This is the result for enabled multithreading, but we should compare the results with a single threaded solution to be sure that we reached real optimization. For this task we can simply change usage of the multithreaded queue to a single threaded queue:

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

The OdRxThreadPoolService::newSTQueue method returns a similar OdApcQueue interface, like the OdRxThreadPoolService::newMTQueue method, so other parts of the code don’t require any changes for performance measurements; OdApcQueue::addEntryPoint and OdApcQueue::wait methods can be invoked as before. The difference with the multithreaded queue is that the single threaded queue executes all requested tasks in one dedicated thread.

Now we can see the different performance measurement results:

4 files loaded and rendered in 1.081148 seconds

Final raster image processed in 1.824529 seconds

The loading and rendering process of our library configuration and environment databases runs approximately two times faster with multithreading enabled. Raster image processing runs approximately three times faster with enabled multithreading. This difference is because raster image processing doesn’t require synchronization for shared resources and memory access between threads as compared with database loading and vectorization processes.

Conclusion

Usage of the Thread Pool Services API can significantly simplify implementations of multithreaded solutions. This API contains not only interfaces for working with threads and queues, it contains a set of interfaces for multithreading synchronization.

For example, take a classic event object implemented in a cross-platform way such as the OdApcEvent interface. OdApcEvent can be used for simple multithreading synchronization tasks. It is useful for shared resource access. For example, the first thread calls the OdApcEvent::reset method before entering a part of the code that can cause conflicts during multithreaded access and calls the OdApcEvent::set method after leaving this part of the code. The second thread can use the OdApcEvent::wait method to wait until the first thread doesn’t call the OdApcEvent::set method and after that enter the part of the code that can cause conflict during multithreaded access and can use the results of the first thread processing. Waiting threads don’t use any CPU resources, so this is a normal solution for synchronization of multiple threads. Usage of thread synchronization objects guarantees that multiple threads will work stably with critical code sections and the results of multithreaded processing will not be corrupted.

The Thread Pool Services API also contains more complex synchronization objects: OdApcGateway and OdApcLoopedGateway. But their description is beyond the scope of this article.

A future blog article will continue this discussion about Teigha multithreading capabilities. It will describe the low-level Teigha Kernel multithreading API and how it communicates with the high-level Thread Pool Services API.