多线程 Direct2D 应用

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

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

注意

如果访问从两个不同的单线程 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 工厂,但必须执行操作以避免访问冲突。

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

注意

在这种情况下,Direct2D 提供的线程保护 (此映像中的蓝色锁) 不起作用。

 

线程保护关系图。

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

注意

请务必配对对 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 被阻止。 在底部行中,Thread 1 之后的最终屏幕外呈现会恢复使用已更改的画笔和已更改的文本字符串绘制下半部分。

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

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

总结

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