【问题标题】:C# Object Pooling With Interlocked.Increment具有 Interlocked.Increment 的 C# 对象池
【发布时间】:2010-09-07 21:52:43
【问题描述】:

我见过很多好的对象池实现。例如:C# Object Pooling Pattern implementation

但似乎线程安全的总是使用锁并且从不尝试使用 Interlocked.* 操作。

编写一个不允许将对象返回到池中的方法似乎很容易(只是一个带有 Interlocked.Increments 指针的大数组)。但是我想不出任何方法来编写一个让您返回对象的方法。有人做过吗?

【问题讨论】:

    标签: c# .net multithreading locking interlocked


    【解决方案1】:

    仔细想想为什么需要对象池 - 这里没有讨论池化的对象。对于大多数对象,使用托管堆将提供必要的功能,而无需在您自己的代码中使用新的池管理器。只有当您的对象封装了难以建立或难以释放的资源时,托管代码中的对象池才值得考虑。

    如果您确实需要自己做,那么有一个轻量级的读取器/写入器锁可能有助于优化池访问。

    http://theburningmonk.com/2010/02/threading-using-readerwriterlockslim/

    【讨论】:

    • 现在我在我的对象池类中使用了一个常规锁,它的速度大约是读/写锁 slim (albahari.com/threading/part2.aspx) 的两倍。但我们只谈论 20 与 40 ns。对于大多数用途来说,两者都足够快。我主要只是好奇是否有人知道如何使用 Interlocked.* 语义来制作一个更快的。
    【解决方案2】:

    我已经通过构建为单链表的无锁队列来完成它。下面删掉了一些不相关的东西,我没有在删除这些东西的情况下对其进行测试,但至少应该给出这个想法。

    internal sealed class LockFreeQueue<T>
    {
      private sealed class Node
      {
        public readonly T Item;
        public Node Next;
        public Node(T item)
        {
          Item = item;
        }
      }
      private volatile Node _head;
      private volatile Node _tail;
      public LockFreeQueue()
      {
        _head = _tail = new Node(default(T));
      }
    #pragma warning disable 420 // volatile semantics not lost as only by-ref calls are interlocked
      public void Enqueue(T item)
      {
        Node newNode = new Node(item);
        for(;;)
        {
          Node curTail = _tail;
          if (Interlocked.CompareExchange(ref curTail.Next, newNode, null) == null)   //append to the tail if it is indeed the tail.
          {
            Interlocked.CompareExchange(ref _tail, newNode, curTail);   //CAS in case we were assisted by an obstructed thread.
            return;
          }
          else
          {
            Interlocked.CompareExchange(ref _tail, curTail.Next, curTail);  //assist obstructing thread.
          }
        }
      }    
      public bool TryDequeue(out T item)
      {
        for(;;)
        {
          Node curHead = _head;
          Node curTail = _tail;
          Node curHeadNext = curHead.Next;
          if (curHead == curTail)
          {
            if (curHeadNext == null)
            {
              item = default(T);
              return false;
            }
            else
              Interlocked.CompareExchange(ref _tail, curHeadNext, curTail);   // assist obstructing thread
          }
          else
          {
            item = curHeadNext.Item;
            if (Interlocked.CompareExchange(ref _head, curHeadNext, curHead) == curHead)
            {
              return true;
            }
          }
        }
      }
    #pragma warning restore 420
    }
    

    如果您使用池的原因是出于对分配和收集的原始性能考虑,那么这个分配和收集的事实使它变得毫无用处。如果是因为获取和/或释放底层资源的成本很高,或者因为实例缓存了正在使用的“学习”信息,那么它可能适合。

    【讨论】:

    • 感谢您的示例!我进行池化的原因是在分配期间可能会收集。因此,由于分配,这并不完全有效。但是类似的东西......
    • 分配时collection有什么问题?它不会收集正在分配的内容。如果您可以将类本身设计为以这种方式工作(而不是需要在不更改类的情况下添加支持),那么您可以按照相同的方式轻松构建一个 freelist。如果这不仅仅是一个实验,那么你很可能解决了错误的问题。如果您需要颠覆 GC 所需的那种精确的执行控制,那么您最好不要在线程之间共享。请记住,虽然多线程可以提高共享效率...
    • ... 的任务,单个线程本身总是更高效,如果不是更灵敏的话。我只能想到几个将 GC 破坏到那种程度的情况,它们都使所有线程完全分离。
    • 这就是说,如果你要对GC进行这么多的颠覆,你必须为整个系统做这件事。否则,其他事情可能会触发收集。这本身就会排除一些最好的无锁算法。上面的例子不能直接移植到非 GC 框架,因为更直接的翻译会遇到 ABA 问题(实际上,它接近于我见过的演示 ABA 问题的示例!)。正是 GC 使上述剩余的 ABA 问题不再是问题(C++ 中丢失的指针在这里只是 GC 素材)。
    【解决方案3】:

    返回引用对象的问题在于它首先破坏了对它的锁定访问的整个尝试。您不能使用基本的 lock() 命令来控制对对象范围之外的资源的访问,这意味着传统的 getter/setter 设计不起作用。

    可以工作的东西是一个包含可锁定资源的对象,并允许传入 lambda 或委托以使用该资源。该对象将锁定资源,运行委托,然后在委托完成时解锁。这基本上将运行代码的控制权交给了锁定对象,但允许比 Interlocked 更复杂的操作。

    另一种可能的方法是公开 getter 和 setter,但通过使用“checkout”模型实现您自己的访问控制;当允许线程“获取”一个值时,将当前线程的引用保存在锁定的内部资源中。在该线程调用 setter、中止等之前,所有其他尝试访问 getter 的线程都保留在 Yield 循环中。一旦资源被重新签入,下一个线程就可以获取它。

    public class Library
    {
       private Book controlledBook
       private Thread checkoutThread;
    
       public Book CheckOutTheBook()
       {
          while(Thread.CurrentThread != checkoutThread && checkoutThread.IsAlive)
              thread.CurrentThread.Yield();
    
          lock(this)
          {
             checkoutThread = Thread.CurrentThread;
    
             return controlledBook;
          }
       }
    
       public void CheckInTheBook(Book theBook)
       {
          if(Thread.CurrentThread != checkoutThread)
              throw new InvalidOperationException("This thread does not have the resource checked out.");
    
          lock(this)
          {
             checkoutThread = null;
    
             controlledBook = theBook;
          }
       }
    
    }
    

    现在,请注意,这仍然需要对象用户之间的一些合作。特别是,这种逻辑对于 setter 来说是相当幼稚的;没有签出就不可能签入一本书。这条规则对消费者来说可能并不明显,不当使用可能会导致未处理的异常。此外,所有用户都必须知道如果他们将在终止之前停止使用该对象,则必须重新签入该对象,即使基本的 C# 知识表明,如果您获得引用类型,您所做的更改也会反映在任何地方。但是,这可以用作对非线程安全资源的基本“一次一个”访问控制。

    【讨论】:

    • 永远不要锁定this,除非是在私有内部类中(即使那样......),因为它会与同一实例上的外部代码阻塞冲突。此外,结帐线程技术只会导致长期监视器提供的同一事物的非惯用版本。在对象对调用者可见之前,您也可以通过控制集合内部的访问来不需要此类用户合作。
    • @Jon Hanna:如果某个类的用户需要独占访问该类的实例,那么他们最自然地锁定的就是该对象实例。如果该类希望为其用户尊重这种锁定语义,那么最自然的方法不是让它锁定“this”吗?是的,可能会有问题,但如果设计要求外部人员能够要求独占访问,还有什么替代方案?
    • @supercat 用户确实应该锁定有问题的实例,这正是为什么类中的代码不应该锁定this 的原因,因为这样两个不同的代码段可能会产生令人讨厌的交互。假设从外部可以随时在对象上持有锁,而从内部锁定同样是最自然的锁定对象的内部对象,或者如果没有合适的对象锁定仅为此而存在的对象原因。不要锁定其他代码可见的东西,只锁定私有对象。
    【解决方案4】:

    你看过 .Net 4 中的并发集合吗?

    例如http://msdn.microsoft.com/en-us/library/dd287191.aspx

    【讨论】:

      【解决方案5】:

      好问题。在编写采用零分配模式的高性能软件时,使用快速对象池至关重要。

      微软在 Apache License 2.0 下发布了一个对象池

      它避免使用锁,并且只使用 Interlocked.CompareExchange 进行分配 (Get)。当您一次获取和释放几个对象时,这似乎特别快,这是大多数用例。如果您获得大量对象,然后释放该批次对象,则似乎优化程度较低,因此如果您的应用程序行为如此,您应该修改。

      我认为,正如您所建议的,Interlocked.Increment 方法可能更通用,并且更适合批处理用例。

      http://sourceroslyn.io/#Microsoft.CodeAnalysis.Workspaces/ObjectPool%25601.cs,98aa6d9b3c4e313b

      // Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.
      
      // define TRACE_LEAKS to get additional diagnostics that can lead to the leak sources. note: it will
      // make everything about 2-3x slower
      // 
      // #define TRACE_LEAKS
      
      // define DETECT_LEAKS to detect possible leaks
      // #if DEBUG
      // #define DETECT_LEAKS  //for now always enable DETECT_LEAKS in debug.
      // #endif
      
      using System;
      using System.Diagnostics;
      using System.Threading;
      
      #if DETECT_LEAKS
      using System.Runtime.CompilerServices;
      
      #endif
      namespace Microsoft.CodeAnalysis.PooledObjects
      {
          /// <summary>
          /// Generic implementation of object pooling pattern with predefined pool size limit. The main
          /// purpose is that limited number of frequently used objects can be kept in the pool for
          /// further recycling.
          /// 
          /// Notes: 
          /// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there
          ///    is no space in the pool, extra returned objects will be dropped.
          /// 
          /// 2) it is implied that if object was obtained from a pool, the caller will return it back in
          ///    a relatively short time. Keeping checked out objects for long durations is ok, but 
          ///    reduces usefulness of pooling. Just new up your own.
          /// 
          /// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. 
          /// Rationale: 
          ///    If there is no intent for reusing the object, do not use pool - just use "new". 
          /// </summary>
          internal class ObjectPool<T> where T : class
          {
              [DebuggerDisplay("{Value,nq}")]
              private struct Element
              {
                  internal T Value;
              }
      
              /// <remarks>
              /// Not using System.Func{T} because this file is linked into the (debugger) Formatter,
              /// which does not have that type (since it compiles against .NET 2.0).
              /// </remarks>
              internal delegate T Factory();
      
              // Storage for the pool objects. The first item is stored in a dedicated field because we
              // expect to be able to satisfy most requests from it.
              private T _firstItem;
              private readonly Element[] _items;
      
              // factory is stored for the lifetime of the pool. We will call this only when pool needs to
              // expand. compared to "new T()", Func gives more flexibility to implementers and faster
              // than "new T()".
              private readonly Factory _factory;
      
      #if DETECT_LEAKS
              private static readonly ConditionalWeakTable<T, LeakTracker> leakTrackers = new ConditionalWeakTable<T, LeakTracker>();
      
              private class LeakTracker : IDisposable
              {
                  private volatile bool disposed;
      
      #if TRACE_LEAKS
                  internal volatile object Trace = null;
      #endif
      
                  public void Dispose()
                  {
                      disposed = true;
                      GC.SuppressFinalize(this);
                  }
      
                  private string GetTrace()
                  {
      #if TRACE_LEAKS
                      return Trace == null ? "" : Trace.ToString();
      #else
                      return "Leak tracing information is disabled. Define TRACE_LEAKS on ObjectPool`1.cs to get more info \n";
      #endif
                  }
      
                  ~LeakTracker()
                  {
                      if (!this.disposed && !Environment.HasShutdownStarted)
                      {
                          var trace = GetTrace();
      
                          // If you are seeing this message it means that object has been allocated from the pool 
                          // and has not been returned back. This is not critical, but turns pool into rather 
                          // inefficient kind of "new".
                          Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nPool detected potential leaking of {typeof(T)}. \n Location of the leak: \n {GetTrace()} TRACEOBJECTPOOLLEAKS_END");
                      }
                  }
              }
      #endif
      
              internal ObjectPool(Factory factory)
                  : this(factory, Environment.ProcessorCount * 2)
              { }
      
              internal ObjectPool(Factory factory, int size)
              {
                  Debug.Assert(size >= 1);
                  _factory = factory;
                  _items = new Element[size - 1];
              }
      
              private T CreateInstance()
              {
                  var inst = _factory();
                  return inst;
              }
      
              /// <summary>
              /// Produces an instance.
              /// </summary>
              /// <remarks>
              /// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
              /// Note that Free will try to store recycled objects close to the start thus statistically 
              /// reducing how far we will typically search.
              /// </remarks>
              internal T Allocate()
              {
                  // PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements.
                  // Note that the initial read is optimistically not synchronized. That is intentional. 
                  // We will interlock only when we have a candidate. in a worst case we may miss some
                  // recently returned objects. Not a big deal.
                  T inst = _firstItem;
                  if (inst == null || inst != Interlocked.CompareExchange(ref _firstItem, null, inst))
                  {
                      inst = AllocateSlow();
                  }
      
      #if DETECT_LEAKS
                  var tracker = new LeakTracker();
                  leakTrackers.Add(inst, tracker);
      
      #if TRACE_LEAKS
                  var frame = CaptureStackTrace();
                  tracker.Trace = frame;
      #endif
      #endif
                  return inst;
              }
      
              private T AllocateSlow()
              {
                  var items = _items;
      
                  for (int i = 0; i < items.Length; i++)
                  {
                      // Note that the initial read is optimistically not synchronized. That is intentional. 
                      // We will interlock only when we have a candidate. in a worst case we may miss some
                      // recently returned objects. Not a big deal.
                      T inst = items[i].Value;
                      if (inst != null)
                      {
                          if (inst == Interlocked.CompareExchange(ref items[i].Value, null, inst))
                          {
                              return inst;
                          }
                      }
                  }
      
                  return CreateInstance();
              }
      
              /// <summary>
              /// Returns objects to the pool.
              /// </summary>
              /// <remarks>
              /// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
              /// Note that Free will try to store recycled objects close to the start thus statistically 
              /// reducing how far we will typically search in Allocate.
              /// </remarks>
              internal void Free(T obj)
              {
                  Validate(obj);
                  ForgetTrackedObject(obj);
      
                  if (_firstItem == null)
                  {
                      // Intentionally not using interlocked here. 
                      // In a worst case scenario two objects may be stored into same slot.
                      // It is very unlikely to happen and will only mean that one of the objects will get collected.
                      _firstItem = obj;
                  }
                  else
                  {
                      FreeSlow(obj);
                  }
              }
      
              private void FreeSlow(T obj)
              {
                  var items = _items;
                  for (int i = 0; i < items.Length; i++)
                  {
                      if (items[i].Value == null)
                      {
                          // Intentionally not using interlocked here. 
                          // In a worst case scenario two objects may be stored into same slot.
                          // It is very unlikely to happen and will only mean that one of the objects will get collected.
                          items[i].Value = obj;
                          break;
                      }
                  }
              }
      
              /// <summary>
              /// Removes an object from leak tracking.  
              /// 
              /// This is called when an object is returned to the pool.  It may also be explicitly 
              /// called if an object allocated from the pool is intentionally not being returned
              /// to the pool.  This can be of use with pooled arrays if the consumer wants to 
              /// return a larger array to the pool than was originally allocated.
              /// </summary>
              [Conditional("DEBUG")]
              internal void ForgetTrackedObject(T old, T replacement = null)
              {
      #if DETECT_LEAKS
                  LeakTracker tracker;
                  if (leakTrackers.TryGetValue(old, out tracker))
                  {
                      tracker.Dispose();
                      leakTrackers.Remove(old);
                  }
                  else
                  {
                      var trace = CaptureStackTrace();
                      Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nObject of type {typeof(T)} was freed, but was not from pool. \n Callstack: \n {trace} TRACEOBJECTPOOLLEAKS_END");
                  }
      
                  if (replacement != null)
                  {
                      tracker = new LeakTracker();
                      leakTrackers.Add(replacement, tracker);
                  }
      #endif
              }
      
      #if DETECT_LEAKS
              private static Lazy<Type> _stackTraceType = new Lazy<Type>(() => Type.GetType("System.Diagnostics.StackTrace"));
      
              private static object CaptureStackTrace()
              {
                  return Activator.CreateInstance(_stackTraceType.Value);
              }
      #endif
      
              [Conditional("DEBUG")]
              private void Validate(object obj)
              {
                  Debug.Assert(obj != null, "freeing null?");
      
                  Debug.Assert(_firstItem != obj, "freeing twice?");
      
                  var items = _items;
                  for (int i = 0; i < items.Length; i++)
                  {
                      var value = items[i].Value;
                      if (value == null)
                      {
                          return;
                      }
      
                      Debug.Assert(value != obj, "freeing twice?");
                  }
              }
          }
      }
      

      【讨论】:

        【解决方案6】:

        我看不出使用 Interlocked 有什么真正的好处,因为它必须以不安全的方式使用。锁定,只是更改对象内存空间上的一个位标志 - 确实非常非常快。互锁稍微好一点,因为它可以在寄存器上而不是在内存中完成。

        您是否遇到性能问题?此类代码的主要目的是什么?归根结底,C# 旨在从您那里抽象出内存管理,以便您专注于业务问题。

        请记住,如果您需要自己管理内存并使用不安全的指针,则必须固定内存区域 = 额外的性能成本。

        【讨论】:

        • 我不确定你所说的 lock 语句“只是改变了一点标志......确实非常非常快”是什么意思。监视器锁是 C# 中相对昂贵的操作。 (这正是有争议的双重检查锁定模式存在的原因)
        • 你在哪里读到的?我的参考是 Jeffrey Richter 通过 C# 编写的 CLR。如果你愿意,我可以给你一个 sn-p。
        • "考虑使用 Interlocked 类的方法进行简单的状态更改,而不是使用 lock 语句(Visual Basic 中的 SyncLock)。lock 语句是一个很好的通用工具,但 Interlocked 类提供必须是原子更新的更好性能。” msdn.microsoft.com/en-us/library/1c9txz50.aspx 还有:informit.com/guides/content.aspx?g=dotnet&seqNum=600
        • Interlocked 需要谨慎使用,但它本身并不是不安全的,尽管它更难证明正确性。从 .NET 的角度来看,这绝对不是不安全的。与内存读取相比,监视器锁确实相对昂贵,这就是存在双重检查模式的原因(这不公平地借用了关于 Java 不同内存模型的事实的大部分争议,但 OTOH 它并没有那么昂贵所以很多时候确实毫无意义)。竞争锁比 some 无锁模式要昂贵得多,尽管不是全部。无需通过互锁锁定内存。
        • 互锁不是不安全的,使用指针是不安全的。当您需要使用不安全的代码时需要固定内存,即使用指针。再次与互锁无关。请再次阅读我的回答。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-05
        • 2023-04-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多