6

我正在开发一个需要大量 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 using 块中,因此我们一直在锁定,分析表明这种锁定占运行时完成工作的近 50%。在我看来,这是一个足够基本的概念,.NET 或 CLR 中的某些东西必须解决它。有没有更好的方法来做引用计数?

4

7 回答 7

8

这是一项比您乍看之下可能预期的更棘手的任务。我不相信 Interlocked.Increment 足以完成您的任务。相反,我希望您需要使用 CAS(比较和交换)来执行一些魔法。

还要注意,很容易得到这个大部分正确,但是当你的程序因 heisenbugs 而崩溃时,大部分正确仍然是完全错误的。

我强烈建议在走这条路之前进行一些真正的研究。如果您搜索“无锁引用计数”,那么几个不错的起点会出现在顶部。 Dobbs 博士的这篇文章很有用,这个 SO Question可能是相关的。

最重要的是,请记住无锁编程很难。如果这不是您的专长,请考虑退后一步并围绕参考计数的粒度调整您的期望。如果您不是专家,重新考虑您的基本引用计数策略可能比创建可靠的无锁机制要便宜得多。尤其是当您还不知道无锁技术实际上会更快时。

于 2012-04-09T15:16:26.127 回答
1

正如哈罗德的评论指出的那样,答案是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
    }
  }
}

两者都Increment返回Decrement新计数(仅用于这种用法),因此进行不同的检查。

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

于 2012-04-09T13:55:30.750 回答
0

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

于 2012-04-09T13:49:59.547 回答
0

我相信这将为您提供使用 Interlocked.Increment/Decrement 的安全方式。

注意:这过于简单了,如果 Init() 抛出异常,下面的代码可能会导致死锁。当计数变为零时,还有一个竞争条件,初始化被重置并再次调用构造函数。Dispose我不知道您的程序流程,因此如果您有可能在几次 dispose 调用后再次初始化,那么您最好使用像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,所以只需调用它就可以了。

于 2012-04-09T13:55:54.893 回答
0

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

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

希望这对您有所帮助。

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);
    }
}
于 2012-04-10T09:26:05.613 回答
0

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

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

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

我没有尝试过像上面这样使用 using 的方法ConcurrentBag,所以我不知道它实际上会产生什么样的性能,但根据使用模式,它可能会提供比通过引用计数获得的性能更好的性能。

于 2012-04-12T01:05:34.580 回答
0

Interlocked class 方法的工作速度比 lock 语句快一点,但在多核机器上速度优势可能不是很大,因为 Interlocked 指令必须绕过内存缓存层。

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

通常,您只需将调用 Init() 一次放在包装其他 API 的类的静态构造函数中,而不必真正担心调用 Term()。例如:

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

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

还可以挂钩某些(但不是全部)应用程序关闭场景的通知,从而可以在干净关闭时调用 Term()。见这篇文章。http://www.codeproject.com/Articles/16164/Managed-Application-Shutdown

于 2012-04-23T17:08:07.860 回答