【问题标题】:Eagerly Disposing a ManualResetEvent急切地处理 ManualResetEvent
【发布时间】:2011-06-08 01:44:28
【问题描述】:

我有一个类允许其他线程等待,直到它使用ManualResetEventSlim 完成操作。 (操作通常很简短)

这个类没有明确的生命周期,所以没有一个地方可以让我轻松关闭事件。
相反,我想在事件完成后立即关闭它——一旦它发出信号,并且在任何等待线程唤醒之后。

出于性能原因,我不想使用锁。

这段代码是线程安全的吗?可以做得更快吗?

volatile bool isCompleted;
volatile int waitingCount;
ManualResetEventSlim waiter = new ManualResetEventSlim();

//This method is called on any thread other than the one that calls OnCompleted
public void WaitForCompletion() {
    if (isCompleted)
        return;

    Interlocked.Increment(ref waitingCount);
    Thread.MemoryBarrier();
    if (!isCompleted)
        waiter.Wait();

    if (0 == Interlocked.Decrement(ref waitingCount)) {
        waiter.Dispose();
        waiter = null;
    }
    return;
}

//This method is called exactly once.
protected internal virtual void OnCompleted(string result) {
    Result = result;
    isCompleted = true;
    Thread.MemoryBarrier();
    if (waitingCount == 0) {
        waiter.Dispose();
        waiter = null;
    } else
        waiter.Set();
}

【问题讨论】:

  • 进一步看,如果同时调用这两种方法,我认为它可能会双重处理服务员。
  • 在所有符合 IDisposable 的实现中多次调用 Dispose 是安全的。
  • @six:是的,但我也是nulling 变量。但是,这可能不是必需的。
  • @SLaks:我会避免nulling,我发现它没有帮助,最终会在某个时候引入一个奇怪的角落案例。
  • 另外,ManualResetEventSlim.Dispose 不是线程安全的; 一次调用两次可能不太合适。

标签: c# .net thread-safety


【解决方案1】:

我在您的代码中看到的最重要的事情是在调用Dispose 之后将waiter 设置为null。我在我负责的非托管接口上有大量托管包装器,当我迁移到 .Net 4.0 时,这种做法又在某些线程场景中对我产生了影响。

ManualResetEventSlim.Dispose 上的 MSDN 信息表明它不是线程安全的,但是,查看其实际实现,从多个线程多次调用 Dispose 并没有什么危险。此外,IDisposable 的实现应该非常容忍多次调用(如其设计指南中所述)。

我曾尝试过的一个想法是对OnCompleted 稍微重新排序,以便读者在完成后不久订阅:

//This method is called exactly once.
protected internal virtual void OnCompleted(string result) {
    Result = result;
    isCompleted = true;

    waiter.Set();
    Thread.MemoryBarrier();
    if (waitingCount == 0) {
        waiter.Dispose();
    }
}

【讨论】:

  • 阅读器在完成后不久就无法订阅。如果waitingCount 已经为零,那么任何服务员都会在调用Wait() 之前看到isCompletedtrue
  • @SLaks:我正在解决这个问题的论文上有些混乱 :) 我已经从我的回答中看出了这一点。
【解决方案2】:

更糟糕的是,在某些情况下它根本不会处理服务员。如果您在waitingCount > 0 时调用OnCompletedisCompleted 标志将设置为true,但不会处理服务员。当有东西调用WaitForCompletion时,它会看到isCompletedtrue,并立即退出。 waiter.Dispose 永远不会被调用。

为什么不使用类似SpinLock 的东西,它使用与ManualResetEventSlim 相同的逻辑?如果您的等待时间通常很短,那么锁可能不会被争用,这是一个巨大的胜利。如果等待时间很长,那么ManualResetEventSlim 无论如何都要为内核转换付出代价。

您确定使用锁会非常昂贵吗?有“知道”,然后有测量。 . .

【讨论】:

  • 不,我不确定;如果可能的话,我想避免一个。
  • 我已经在考虑使用SpinLock;我只是问了这个问题,看看是否有任何聪明的选择。
  • 经过进一步研究,我不确定我上面的说法是否属实。也就是说,可能无法让服务员不被处理。但你是对的,存在可能导致双重处置的竞争条件。
  • 我不认为你的第一点是正确的。如果waitingCount > 0,最后一个等待完成的调用将释放服务员。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-02-27
  • 2011-05-28
  • 1970-01-01
  • 1970-01-01
  • 2011-12-16
  • 2021-02-17
  • 2022-01-13
相关资源
最近更新 更多