无锁参考计数

时间:2012-04-09 13:35:34

标签: c# .net concurrency locking reference-counting

我正在开发一个需要广泛的C API互操作的系统。部分互操作需要在任何操作之前和之后初始化和关闭相关系统。如果不这样做将导致系统不稳定。我通过简单地在这样的核心一次性环境类中实现引用计数来实现这一点:

public FooEnvironment()
{
  lock(EnvironmentLock)
  {
    if(_initCount == 0)
    {
      Init();  // global startup
    }
    _initCount++;
  }
}

private void Dispose(bool disposing)
{
  if(_disposed)
    return;

  if(disposing)
  {
    lock(EnvironmentLock)
    {
      _initCount--;
      if(_initCount == 0)
      {
        Term(); // global termination
      }
    }
  }
}

这很好,完成了目标。但是,由于任何互操作操作必须使用块嵌套在FooEnvironment中,因此我们始终锁定并且分析表明此锁定占运行时期间完成工作的近50%。在我看来,这是一个基本的概念,.NET或CLR中的某些东西必须解决它。有没有更好的方法来进行引用计数?

7 个答案:

答案 0 :(得分:5)

这是一个比你第一次脸红所期望的更棘手的任务。我不相信Interlocked.Increment足以完成你的任务。相反,我希望你需要使用CAS(比较和交换)执行一些魔法。

另请注意,非常容易来实现这一目标,但是当你的程序与heisenbugs崩溃时,大多数情况下仍然是完全错误的。

在走这条路之前,我强烈建议进行一些真正的研究。如果你搜索“锁定免费引用计数”,一对好的跳跃点会弹到顶部。 This Dr. Dobbs article很有用,this SO Question可能相关。

最重要的是,请记住锁定免费编程很难。如果这不是您的专业,请考虑退回并根据参考计数的粒度调整您的期望。如果您不是专家,那么重新考虑您的基本引用计数政策可能要比创建可靠的无锁机制要便宜得多。特别是当你还不知道无锁技术实际上会更快。

答案 1 :(得分:2)

正如harold's评论所说,答案是Interlocked

public FooEnvironment() {
  if (Interlocked.Increment(ref _initCount) == 1) {
    Init();  // global startup
  }
}

private void Dispose(bool disposing) {
  if(_disposed)
    return;

  if (disposing) {
    if (0 == Interlocked.Decrement(ref _initCount)) {
      Term(); // global termination
    }
  }
}

IncrementDecrement都返回新计数(仅用于此类用法),因此会进行不同的检查。

但请注意:如果还有其他需要并发保护的话,这将无法正常工作Interlocked操作本身是安全的,但没有其他操作(包括Interlocked调用的不同线程相对排序)。在上面的代码中,Init()仍然可以在另一个线程完成构造函数后运行。

答案 2 :(得分:0)

可能在类中使用一般的静态变量。静态只是一件事,并不是特定于任何对象。

答案 3 :(得分:0)

我相信这会给你一个使用Interlocked.Increment / Decrement的安全方式。

注意:这是过于简单的,如果Init()抛出异常,下面的代码可能会导致死锁。当计数变为零时,Dispose中还存在竞争条件,重置init并再次调用构造函数。我不知道你的程序流程,所以你可能最好使用更便宜的锁,如SpinLock,而不是InterlockedIncrement,如果你有可能在几次处理后再次启动。

static ManualResetEvent _inited = new ManualResetEvent(false);
public FooEnvironment()
{
    if(Interlocked.Increment(ref _initCount) == 1)
    {
        Init();  // global startup
        _inited.Set();
    }

    _inited.WaitOne();
}

private void Dispose(bool disposing)
{
    if(_disposed)
        return;

    if(disposing)
    {
        if(Interlocked.Decrement(ref _initCount) == 0)
        {
            _inited.Reset();
            Term(); // global termination
        }
    }
}

修改
在进一步思考这个问题时,您可能需要考虑重新设计一些应用程序,而不是使用此类来管理Init和Term,只需在应用程序启动时调用Init,并在应用程序关闭时调用Term,然后删除需要完全锁定,并且如果锁定显示为执行时间的50%,那么您似乎总是想要调用Init,所以只需要调用它然后就可以了。

答案 4 :(得分:0)

您可以使用以下代码使其几乎无锁。它肯定会降低争用,如果这是你的主要问题,它将是你需要的解决方案。

另外我建议从析构函数/终结器中调用Dispose(以防万一)。我已经更改了Dispose方法 - 无论disposing参数如何,都应释放非托管资源。检查this以获取有关如何正确处理对象的详细信息。

希望这会对你有所帮助。

public class FooEnvironment
{
    private static int _initCount;
    private static bool _initialized;
    private static object _environmentLock = new object();

    private bool _disposed;

    public FooEnvironment()
    {
        Interlocked.Increment(ref _initCount);

        if (_initCount > 0 && !_initialized)
        {
            lock (_environmentLock)
            {
                if (_initCount > 0 && !_initialized)
                {
                    Init(); // global startup
                    _initialized = true;
                }
            }
        }
    }

    private void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            // Dispose managed resources here
        }

        Interlocked.Decrement(ref _initCount);

        if (_initCount <= 0 && _initialized)
        {
            lock (_environmentLock)
            {
                if (_initCount <= 0 && _initialized)
                {
                    Term(); // global termination
                    _initialized = false;
                }
            }
        }

        _disposed = true;
    }

    ~FooEnvironment()
    {
        Dispose(false);
    }
}

答案 5 :(得分:0)

使用Threading.Interlocked.Increment比获取锁定,执行增量和释放锁定要快一些,但并非如此。多核系统上任一操作的昂贵部分都是强制核之间的内存缓存同步。 Interlocked.Increment的主要优点不是速度,而是它将在有限的时间内完成的事实。相比之下,如果一个人试图获得一个锁,执行一个增量,并释放锁,即使该锁被用于除了守护计数器之外没有其他目的,如果某个其他线程有可能需要永远等待获得锁定,然后得到锁定。

您没有提到您正在使用的.net版本,但有一些Concurrent类可能有用。根据您分配和释放事物的模式,ConcurrentBag类可能看起来有点棘手但可以正常工作的类。它有点像队列或堆栈,除了不能保证任何特定订单都会出现问题。在资源包装器中包含一个标志,指示它是否仍然良好,并在资源本身中包含对包装器的引用。创建资源用户时,将包装器对象放入包中。当不再需要资源用户时,设置“invalid”标志。只要在包中设置了“valid”标志的至少一个包装器对象,或者资源本身保存对有效包装器的引用,资源就应该保持活动状态。如果删除某个项目时资源似乎没有包含有效的包装器,则获取一个锁定,如果该资源仍然没有保存有效的包装器,则将包装器从包中取出,直到找到有效的包装器,然后用资源存储一个(或者,如果没有找到,则销毁资源)。如果删除某个项目时,资源会保留一个有效的包装器,但该包似乎可能包含过多的无效项目,获取锁定,将包裹的内容复制到数组中,然后将有效的项目放回包中。记下多少物品被扔回去,这样就可以判断何时进行下一次清洗。

这种方法看起来比使用锁或Threading.Interlocked.Increment更复杂,并且需要担心很多极端情况,但它可能提供更好的性能,因为ConcurrentBag旨在减少资源争用。如果处理器1在某个位置执行Interlocked.Increment,然后处理器2执行此操作,则处理器2将必须指示处理器1从其高速缓存中刷新该位置,等待处理器1已经这样做,通知所有其他处理器它需要控制该位置,将该位置加载到其缓存中,最后绕过它来递增它。在发生所有这些之后,如果处理器1需要再次增加位置,则将需要相同的一般步骤序列。所有这一切都非常缓慢。相比之下,ConcurrentBag类的设计使得多个处理器可以在没有缓存冲突的情况下将内容添加到列表中。在添加内容和删除内容之间的某个时间,它们必须被复制到一致的数据结构中,但是这样的操作可以批量执行,以便产生良好的缓存性能。

我没有尝试使用ConcurrentBag之类的方法,因此我不知道它实际会产生什么样的性能,但根据使用模式,可能会提供比将通过参考计数获得。

答案 6 :(得分:0)

互锁类方法的工作速度比锁定法快一点,但在多核机器上,速度优势可能不会太大,因为互锁指令必须绕过内存缓存层。

当代码未被使用和/或程序退出时,调用Term()函数有多重要?

通常,您可以将{(3}}中的Init()调用一次用于包装其他API的类,而不是真的担心调用Term()。 E.g:

static FooEnvironment() { 
    Init();  // global startup 
}

CLR将确保在封闭类中的任何其他成员函数之前调用静态构造函数一次。

还可以挂钩某些(但不是全部)应用程序关闭方案的通知,从而可以在干净关闭时调用Term()。看到这篇文章。 static constructor