如何安全地使用在任意线程上进行回调的库

时间:2010-09-23 14:44:39

标签: c# multithreading thread-safety

我有一个像这样的API的库:

public class Library : IDisposable
{
    public Library(Action callback);
    public string Query();
    public void Dispose();
}

在我实例化了Library之后,在任何时候和任何线程上它都可以调用我传递给它的回调。该回调需要调用Query来完成有用的工作。该库只会在处理后停止调用我的回调,但如果在主线程调用Dispose后回调尝试调用Query,则会发生错误的东西

希望允许回调同时在多个线程上运行。没关系。但是我们需要确保在调用Dispose时不能运行任何回调。我认为ReaderWriterLockSlim可能是合适的 - 您需要使用write-lock来调用Dispose,并且回调需要read-locks来调用Query。这里的问题是ReaderWriterLockSlim是IDisposable,我不认为处理它是安全的 - 我永远不会知道没有飞行中的回调只是没有达到获取读取的程度锁定了。

我该怎么办?看起来ReaderWriterLock不是IDisposable,但它自己的文档说你应该使用ReaderWriterLockSlim。我可以尝试用“lock”关键字做一些相同的事情,但这听起来很浪费,很容易搞砸。

PS - 如果您认为是这种情况,请随意说库API不好。我个人更喜欢它保证Dispose会阻塞,直到所有回调都完成。

2 个答案:

答案 0 :(得分:2)

这听起来像是你可以使用自己的API包装的东西,这是最后一段的保证。

基本上,每个回调应该以原子方式注册它正在运行,并检查它是否仍然可以运行 - 然后立即退出(相当于从不被调用)或执行其操作并取消注册它正在运行。

你的Dispose方法只需要阻塞,直到它找到没有任何东西在运行的时间,原子地检查是否有任何东西在运行,如果没有则无效。

我可以想象这只是使用简单的锁定,监视器,Wait / Pulse方法合理完成。你的API包装器会包装它在另一个回调函数中给出的回调,它会完成所有这些,所以你只需要把逻辑放在一个地方。

你明白我的意思吗?我现在没有时间为你实现它,但如果你愿意,我可以详细说明这些想法。

答案 1 :(得分:2)

如果您必须自己尝试,这是一个相当难以解决的问题。我将在这里描述的模式使用CountdownEvent类作为基本同步机制。它在.NET 4.0中提供,或者作为.NET 3.5的Reactive Extensions下载的一部分提供。这门课程是这一类型问题的理想候选人,因为:

  • 它可以保持计数。
  • 它可以等待该计数达到零。

让我来描述一下这种模式。我创建了一个CallbackInvoker类,它只包含两个操作。

  • 它可以使用Invoke操作同步调用回调。
  • 它可以使用FinishAndWait操作接收停止信号并等待确认。

Library类创建并使用CallbackInvoker的实例。任何时候Library都需要调用回调,它应该通过调用Invoke方法来实现。在处理课程时,只需从FinishAndWait方法调用Dispose即可。这是有效的,因为从CountdownEvent发出FinishAndWait信号的那一刻,它以原子方式锁定TryAddCount。这就是等待句柄初始化为1的原因。

public class Library : IDisposable
{
    private CallbackInvoker m_CallbackInvoker;

    public Library(Action callback)
    {
        m_CallbackInvoker = new CallbackInvoker(callback);
    }

    public void Dispose()
    {
        m_CallbackInvoker.FinishAndWait();
    }

    private void DoSomethingThatInvokesCallback()
    {
        m_CallbackInvoker.Invoke();
    }

    private class CallbackInvoker
    {
        private Action m_Callback;
        private CountdownEvent m_Pending = new CountdownEvent(1);

        public CallbackInvoker(Action callback)
        {
            m_Callback = callback;
        }

        public bool Invoke()
        {
            bool acquired = false;
            try
            {
              acquired = m_Pending.TryAddCount();
              if (acquired)
              {
                  if (m_Callback != null)
                  {
                      m_Callback();
                  }
              }
            }
            finally
            {
              if (acquired) m_Pending.Signal();
            }
            return acquired;
        }

        public void FinishAndWait()
        {
            m_Pending.Signal();
            m_Pending.Wait();
        }

    }
}