急切地处理ManualResetEvent

7
我有一个类,允许其他线程使用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();
}

进一步观察,如果同时调用两种方法,我认为它可能会双重释放服务员。 - SLaks
使用所有兼容的IDisposable实现,多次调用Dispose是安全的。 - user7116
另外,ManualResetEventSlim.Dispose 不是线程安全的;同时调用它两次可能不太好。 - SLaks
@SLaks:我现在正在看它,它似乎是线程安全的。在关闭之前,它会锁定内部事件对象。 - user7116
在GDI+/窗口句柄的情况下,我同意它们具有某些线程要求。但是,我认为任何这些用法都应该转移到SafeHandle,以承认它们的关键终结要求(如果您有任何要求的话)。至于像ManualResetEventSlim这样的同步构造,它似乎不支持自由线程处理。根据定义,它将在多线程环境中使用。但是,正如您所指出的,假设可能是错误的 :) - user7116
显示剩余7条评论
2个回答

1

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

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

您是否确信使用锁会过于昂贵?有"知道",还有"测量"...


不,我不确定;如果可能的话,我只想避免一个。 - SLaks
我已经在考虑使用SpinLock了;我只是问了这个问题,看看是否有任何聪明的替代方案。 - SLaks
经进一步研究,我不确定我上面的陈述是否正确。也就是说,可能无法使服务员未被处理。但你说得对,存在竞态条件可能导致双重处理。 - Jim Mischel
我认为你的第一点是不正确的。如果 waitingCount > 0,最后一个 Wait 调用完成后将会释放该等待者。 - SLaks

1
我在你的代码中发现最大的问题是在调用Dispose之后将waiter设置为null。我负责一大部分托管接口的封装,并在切换到.NET 4.0时,在某些线程情况下这种做法会对我产生影响。
MSDN关于ManualResetEventSlim.Dispose的信息表明它不是线程安全的,然而,在查看其实际实现时,从多个线程中多次调用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
@SLaks:我的草稿纸上的计算有些混乱了 :) 我已经从我的答案中删掉了那一部分。 - user7116

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接