多线程应用程序中的最小延迟对象池技术

时间:2008-09-19 05:36:11

标签: c++ multithreading

  1. 在应用程序中,我们有大约30种重复创建的对象。
  2. 他们中的一些人有很长的生命(小时),有些人有短暂的(毫秒)。
  3. 对象可以在一个线程中创建,在另一个线程中销毁。
  4. 有没有人知道在最小创建/破坏延迟,低锁争用和合理内存利用率的意义上什么才是好的池技术?

    追加1。

    1.1。一种类型的对象池/内存分配通常与另一种类型无关(对于异常,请参见1.3)

    1.2。内存分配仅在一次只对一种类型(类)执行,通常对于几个对象。

    1.3。如果一个类型使用指针聚合另一个类型(由于某种原因),这些类型在一个连续的内存中一起分配。

    追加2。

    2.1。已知使用具有每种类型的访问序列化的集合比新/删除更糟糕。

    2.2。应用程序用于不同的平台/编译器,不能使用编译器/平台特定的技巧。

    追加3。

    很明显,最快(具有最低延迟)的实现应该将对象池组织为星型工厂网络。中央工厂是其他螺纹专用工厂的全球工厂。常规对象提供/回收在特定于线程的工厂中更有效,而中央工厂可用于线程之间的对象平衡。

    3.1。在中央工厂和线程特定工厂之间组织通信的最有效方法是什么?

6 个答案:

答案 0 :(得分:4)

我假设您在完成所有创建后测试了代码并测量了代码,并验证了create / destroy实际上是导致问题。这是你应该先做的事。

如果你仍然想要进行对象池,作为第一步,你应该确保你的对象是无状态的,这将是重用对象的先决条件。类似地,您应该确保对象的成员和对象本身没有从创建它的其他线程使用的问题。 (COM STA对象/窗口句柄等)

如果使用Windows和COM,使用系统提供的池的一种方法是编写Free Threaded对象并启用对象池,这将使COM +运行时(以前称为MTS)为您执行此操作。如果您使用像Java这样的其他平台,也许您可​​以使用定义对象应该实现的接口的应用程序服务器,并且COM +服务器可以为您执行池化。

或者您可以滚动自己的代码。但你应该试着找出是否有这种模式,如果是,则使用它而不是下面的内容

如果您需要滚动自己的代码,请创建一个可动态增长的集合,该集合跟踪已创建的对象。最好使用矢量作为集合,因为您只需要添加到集合中,并且可以轻松遍历它以搜索自由对象。 (假设您不删除池中的对象)。根据删除策略更改集合类型(如果使用C ++,则指针/对象引用的向量,以便在同一位置删除和重新创建对象)

应使用标志跟踪每个对象,该标志可以以易失性方式读取并使用互锁功能进行更改以将其标记为正在使用/未使用。

如果使用了所有对象,则需要创建一个新对象并将其添加到集合中。在添加之前,您可以获取锁(临界区),将新对象标记为正在使用并退出锁。

测量并继续 - 可能如果您将上述集合实现为类,则可以轻松地为不同的对象类型创建不同的集合,以减少执行不同工作的线程的锁争用。

最后,你可以实现一个重载的类工厂接口,它可以创建各种池化对象并知道哪个集合包含哪个类

然后您可以从那里优化此设计。

希望有所帮助。

答案 1 :(得分:2)

为了最大限度地减少构造/破坏延迟,您需要完全构造的对象,因此您将消除新的/ ctor / dtor / delete时间。这些“免费”对象可以保存在列表中,因此您只需在末尾弹出/推送元素。

您可以逐个锁定对象池(每种类型一个)。它比系统范围的锁更有效,但没有按对象锁定的开销。

答案 2 :(得分:1)

如果你还没有看过tcmalloc,你可能想看看。将您的实现基于其概念可能是一个良好的开端。要点:

  • 确定一组大小类。 (每个分配将通过使用来自相同或更大尺寸分配的条目来实现。)
  • 每页使用一个尺寸级别。 (页面中的所有实例大小相同。)
  • 使用每线程空闲列表来避免每个alloc / dealloc上的原子操作
  • 当每个线程的空闲列表太大时,将一些实例移回中央空闲列表。尝试从同一页面移回分配。
  • 当每个线程空闲列表为空时,从中心空闲列表中取一些空闲列表。尝试连续录入。
  • 重要提示:您可能知道这一点,但请确保您的设计最大限度地减少虚假分享。

你可以做的其他事情tcmalloc不能:

  • 尝试使用更精细的分配池启用引用的位置。例如,如果一起访问几千个对象,那么最好是它们在内存中靠得很近。 (最小化缓存丢失和TLB错误。)如果从他们自己的threadcache分配这些实例,那么它们应该具有相当好的位置。

  • 如果您事先知道哪些实例将是长寿命的,哪些实例不会,则从单独的线程缓存中分配它们。如果您不知道,则使用threadcache定期复制旧实例以进行分配,并更新对新实例的旧引用。

答案 3 :(得分:0)

如果您猜测了池的首选大小,可以使用使用数组的堆栈结构创建固定大小的池(最快的解决方案)。然后你需要实现四个阶段的对象生命周期硬初始化(和内存分配),软初始化,软清理和硬清理(以及内存释放)。现在是伪代码:

Object* ObjectPool::AcquireObject()
{
    Object* object = 0;
    lock( _stackLock );
    if( _stackIndex )
       object = _stack[ --_stackIndex ];
    unlock( _stackLock );
    if( !object )
       object = HardInit();
    SoftInit( object );
}

void ObjectPool::ReleaseObject(Object* object)
{
     SoftCleanup( object );
    lock( _stackLock );
    if( _stackIndex < _maxSize )
    {
       object = _stack[ _stackIndex++ ];
       unlock( _stackLock );
    }
    else
    {
       unlock( _stack );
       HardCleanup( object );
    }
}

HardInit / HardCleanup方法执行完整对象初始化和销毁​​,只有在池为空或释放的对象因其已满而无法适应池时才会执行它们。 SoftIniti执行对象的软初始化,它仅初始化自释放后可以更改的对象的那些方面。 SoftCleanup方法可以尽快释放对象使用的空闲资源,或者在其所有者驻留在池中时可能变为无效的资源。正如您所看到的,锁定是最小的,只有两行代码(或只有少量指令)。

这四种方法可以在单独的(模板)类中实现,因此您可以针对每种对象类型或用法实现精细调整的操作。此外,您可以考虑使用智能指针在不再需要时自动将对象返回到其池中。

答案 4 :(得分:0)

你试过hoard allocator吗?它在许多系统上提供比默认分配器更好的性能。

答案 5 :(得分:0)

为什么你有多个线程销毁他们没有创建的对象?这是处理对象生命周期的简单方法,但成本可能因使用而有很大差异。

无论如何,如果你还没有开始实现它,至少你可以将创建/销毁功能放在接口后面,这样你可以在以后获得更多信息时测试/更改/优化它。你的系统实际上做了什么。