Teigha 多线程低级 API(第 1 部分,共 4 部分)

之前的博客文章中,我们描述了 Teigha 的高级多线程 API(Teigha 线程池服务),以及如何使用它,基于 Teigha 的应用程序可以通过最少的调用和辅助数据结构来调用多线程操作。但是,一些客户端应用程序和产品拥有自己的多线程机制或调用第三方多线程库。例如,应用程序可以使用开发 QT、Boost 和 MFC 的公司提供的 API 来运行线程。用 .NET 语言编写的产品可以使用内置的 .NET 框架对象来运行线程。此外,某些类型的应用程序在其自己的线程中执行 Teigha 功能(例如,多线程导出、缩略图创建插件、绘图转换器等)。本系列文章描述了如何安全地将第三方多线程功能与 Teigha 结合使用。

即使您的应用程序不调用第三方多线程功能,并且只使用 Teigha 高级多线程 API,本系列文章仍然会有所帮助,例如,本系列中关于使用互斥对象和原子操作的未来文章。

请注意,本系列关于低级 API 的文章包含的示例是基于之前关于Teigha 多线程高级 API 的文章中的源代码示例。

在外部线程中使用 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方法传递一个或多个线程ID,以便在Teigha端注册它们,初始化所需的每个线程缓存等。请注意,线程属性(第三个方法参数)之前已在Teigha多线程高级API的“构建多线程队列”部分中进行了描述。Threads Counter与线程池服务具有相似的含义。当从外部线程使用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);

使用线程计数器的示例

为了说明Threads Counter对象方法的使用,我们将扩展Teigha多线程高级API博客文章中的示例。

先决条件

首先,为了简化我们的示例,我们可以添加自己的线程管理功能。在实际应用程序中,此功能通常要复杂得多。许多应用程序会调用来自其他库(如QT、Boost等)的第三方多线程功能。

在此示例中,我们将调用纯 Microsoft® Windows® API 函数来实现两个辅助类:

  • 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 类,以使用 SimpleWinThreadsPool 而不是 Teigha 线程池服务:

// 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 方法用于启动每线程操作。

修改我们的主示例应用程序函数,以使用 SimpleWinThreadsPool 类而不是线程池服务 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<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 的示例可以完全使用其自己的线程实现启动,并为输入图形生成类似的输出图像,且性能测量(几乎)相似。

请关注即将发布的关于使用内置 Teigha 互斥对象的文章,以继续使用多线程低级 API。

今天就开始行动

免费试用 ODA 软件 60 天。
无风险,无需信用卡。

免费试用