多线程 Direct2D 应用

如果开发 Direct2D 应用,则可能需要从多个线程访问 Direct2D 资源。 在其他情况下,你可能希望使用多线程来获得更好的性能或更好的响应能力(例如使用一个线程进行屏幕显示和单独的线程进行脱机呈现)。

本主题介绍开发多线程 Direct2D 应用的最佳做法,这些应用几乎没有 Direct3D 呈现。 并发问题导致的软件缺陷可能难以跟踪,并且规划多线程策略并遵循此处所述的最佳做法会很有帮助。

注意

如果访问两个 Direct2D 从两个不同的单线程 Direct2D 工厂创建的资源,则只要基础 Direct3D 设备和设备上下文也不同,就不会导致访问冲突。 在谈到本文中的“访问 Direct2D 资源”时,它实际上意味着“访问从同一 Direct2D 设备创建的 Direct2D 资源”,除非另有说明。

开发仅调用 Direct2D API 的 Thread-Safe 应用

可以创建多线程 Direct2D 工厂实例。 可以使用和共享多线程工厂及其来自多个线程的所有资源,但对这些资源(通过 Direct2D 调用)的访问由 Direct2D 序列化,因此不会发生访问冲突。 如果应用仅调用 Direct2D API,则 Direct2D 会自动以最小开销的粒度级别执行此类保护。 在此处创建多线程工厂的代码。

ID2D1Factory* m_D2DFactory;

// Create a Direct2D factory.
HRESULT hr = D2D1CreateFactory(
    D2D1_FACTORY_TYPE_MULTI_THREADED,
    &m_D2DFactory
);

此处的图像显示了 Direct2D 如何序列化两个仅使用 Direct2D API 进行调用的线程。

两个序列化线程的关系图。

使用最少的 Direct3D 或 DXGI 调用开发 Thread-Safe Direct2D 应用

通常,Direct2D 应用也会进行一些 Direct3D 或 DXGI 调用。 例如,显示线程将在 Direct2D 中绘制,然后使用 DXGI 交换链呈现。

在这种情况下,确保线程安全性更为复杂:某些 Direct2D 调用间接访问基础 Direct3D 资源,另一个调用 Direct3D 或 DXGI 的线程可能会同时访问这些资源。 由于这些 Direct3D 或 DXGI 调用无法识别和控制 Direct2D,需要创建多线程 Direct2D 工厂,但必须执行 mor作以避免访问冲突。

此图显示了 Direct3D 资源访问冲突,因为线程 T0 间接通过 Direct2D 调用访问资源,而 T2 通过 Direct3D 或 DXGI 调用直接访问同一资源。

注意

Direct2D 提供的线程保护(此图像中的蓝色锁)在此示例中无济于事。

 

线程保护关系图。

为避免此处的资源访问冲突,建议显式获取 Direct2D 用于内部访问同步的锁,并在线程需要进行 Direct3D 或可能导致访问冲突的 DXGI 调用时应用该锁,如下所示。 具体而言,应特别注意使用异常或基于 HRESULT 返回代码的早期系统的代码。 出于此原因,建议使用 RAII(资源获取是初始化)模式来调用 Enter保留 方法。

注意

请务必将对 EnterLeave 方法的调用配对,否则应用可能会死锁。

 

此处的代码演示了何时锁定和解锁 Direct3D 或 DXGI 调用的示例。

void MyApp::DrawFromThread2()
{
    // We are accessing Direct3D resources directly without Direct2D's knowledge, so we
    // must manually acquire and apply the Direct2D factory lock.
    ID2D1Multithread* m_D2DMultithread;
    m_D2DFactory->QueryInterface(IID_PPV_ARGS(&m_D2DMultithread));
    m_D2DMultithread->Enter();
    
    // Now it is safe to make Direct3D/DXGI calls, such as IDXGISwapChain::Present
    MakeDirect3DCalls();

    // It is absolutely critical that the factory lock be released upon
    // exiting this function, or else any consequent Direct2D calls will be blocked.
    m_D2DMultithread->Leave();
}

注意

某些 Direct3D 或 DXGI 调用(尤其是 IDXGISwapChain::P resent)可能会获取调用函数或方法代码中的锁和/或触发回调。 应注意这一点,并确保此类行为不会导致死锁。 有关详细信息,请参阅 DXGI 概述 主题。

 

direct2d 和 direct3d 线程锁定关系图。

使用 EnterLeave 方法时,调用受自动 Direct2D 和显式锁定的保护,因此应用不会发生访问冲突。

还有其他方法可以解决此问题。 但是,我们建议使用 Direct2D 锁显式保护 Direct3D 或 DXGI 调用,因为它通常提供更好的性能,因为它在更精细的级别保护并发,并在 Direct2D 的覆盖下开销较低。

确保有状态作的原子性

虽然 DirectX 的线程安全功能可以帮助确保没有两个单独的 API 调用同时进行,但还必须确保发出有状态 API 调用的线程不会相互干扰。 下面是一个示例。

  1. 有两行文本要呈现到屏幕上(由线程 0)和屏幕外(按线程 1):第 1 行是“A 更大”,第 2 行是“比 B”,这两行都将使用纯黑色画笔绘制。
  2. 线程 1 绘制第一行文本。
  3. 线程 0 对用户输入做出反应,将文本行分别更新为“B 较小”和“比 A”,并将画笔颜色更改为纯红色作为自己的绘图:
  4. 线程 1 继续绘制第二行文本,即现在为“比 A”,红色画笔:
  5. 最后,我们在屏幕外绘图目标上得到两行文本:黑色“A 更大”,红色为“比 A”。

屏幕线程和屏幕外线程的关系图。

在顶部行中,Thread 0 使用当前文本字符串和当前黑色画笔绘制。 线程 1 仅完成上半部分的屏幕外绘图。

在中间行中,Thread 0 响应用户交互,更新文本字符串和画笔,然后刷新屏幕。 此时,将阻止线程 1。 在底部行中,线程 1 恢复绘制下半部分后的最后一个屏幕外呈现,其中包含已更改的画笔和已更改的文本字符串。

若要解决此问题,建议为每个线程创建单独的上下文,以便:

  • 应创建设备上下文的副本,以便可变资源(即在显示或打印期间可能有所不同的资源,如文本内容或示例中的纯色画笔)在呈现时不会更改。 在此示例中,应在绘制前保留这两行文本和颜色画笔的副本。 这样做可以保证每个线程都有完整且一致的内容来绘制和呈现。
  • 应共享重量级资源(如位图和复杂效果图),这些资源初始化一次,然后从未在线程间修改,以提高性能。
  • 可以共享轻量级资源(如纯色画笔和文本格式),这些资源初始化一次,然后永远不会在线程之间修改

总结

开发多线程 Direct2D 应用时,必须创建多线程 Direct2D 工厂,然后从该工厂派生所有 Direct2D 资源。 如果线程 Direct3D 或 DXGI 调用,则还必须显式获取,然后应用 Direct2D 锁来保护这些 Direct3D 或 DXGI 调用。 此外,必须通过为每个线程提供可变资源的副本来确保上下文完整性。