【问题标题】:How to safely use a library that makes callbacks on arbitrary threads如何安全地使用在任意线程上进行回调的库
【发布时间】:2010-09-23 14:44:39
【问题描述】:

我有一个类似这样的 API 库:

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

在我实例化库之后,它可能随时在任何线程上调用我传递给它的回调。该回调需要调用 Query 来完成有用的工作。库只会在释放后停止调用我的回调,但如果回调在主线程调用 Dispose 后尝试调用 Query,则会发生坏事

确实希望允许回调同时在多个线程上运行。没关系。但是我们需要确保调用 Dispose 时不会运行任何回调。我认为 ReaderWriterLockSlim 可能是合适的——你需要写锁来调用 Dispose,而回调需要读锁来调用 Query。这里的问题是 ReaderWriterLockSlim 是 IDisposable,我认为处置它永远不会安全 - 我永远不知道没有一个回调在飞行中根本没有达到获取读取的地步 -还没有锁定。

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

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

【问题讨论】:

  • lib API 是否包含 IsDisposed 属性?
  • 不,我不这么认为。不过,我不确定它会有什么帮助。需要详细说明吗?

标签: c# multithreading thread-safety


【解决方案1】:

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

基本上,每个回调都应该自动注册它正在运行,并检查它是否仍然可以运行 - 然后立即退出(相当于永远不会被调用)或执行它的操作并取消注册它正在运行。

您的 Dispose 方法只需要阻塞,直到它发现没有任何东西在运行,自动检查是否有任何东西在运行,如果没有,则无效。

我可以想象这合理只需使用简单的锁、监视器、Wait/Pulse 方法即可完成。您的 API 包装器会将其提供的任何回调包装在另一个回调中,该回调执行所有这些操作,因此您只需将逻辑放在一个地方。

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

【讨论】:

  • 一个有趣的想法,但听起来确实有足够的机会进行比赛条件。
  • @Jim:重点是将避免竞争条件的所有复杂性放在一个地方。您仔细编写这个包装器,其余代码可以更简单。
  • 是的,我理解这个概念。不过,必须非常小心地完成。
  • 我想我明白了,尽管我有一种感觉,我仍然可能会搞砸。看起来我可能不必这样做。我想我可以说服库所有者给它一个稍微安全一点的 API。
  • @Weeble:库所有者可能也必须仔细编写它。为什么不尝试实现它,然后在此处发布实现以供审查?
【解决方案2】:

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

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

让我描述一下模式。我创建了一个只包含两个操作的CallbackInvoker 类。

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

Library 类创建并使用CallbackInvoker 的实例。任何时候Library 需要调用回调,它应该通过调用Invoke 方法来实现。当需要处理类时,只需从Dispose 方法调用FinishAndWait。这是有效的,因为当CountdownEventFinishAndWait 发出信号时,它会以原子方式锁定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();
        }

    }
}

【讨论】:

    猜你喜欢
    • 2019-06-27
    • 2019-12-29
    • 2019-12-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-10
    • 2022-08-05
    • 2010-09-05
    相关资源
    最近更新 更多