Как безопасно использовать библиотеку, которая делает обратные вызовы в произвольных потоках - PullRequest
2 голосов
/ 23 сентября 2010

У меня есть библиотека с API, очень похожая на эту:

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

После того, как я создаю экземпляр библиотеки, в любое время и в любом потоке она может вызвать обратный вызов, который я ей передал.Этот обратный вызов должен вызывать Query для выполнения полезной работы.Библиотека прекратит вызывать мой обратный вызов только после удаления, но если обратный вызов попытается вызвать Query после того, как основной поток вызвал Dispose, произойдет плохой материал .

I do хочет разрешить обратные вызовы для одновременного выполнения в нескольких потоках.Это нормально.Но мы должны быть уверены, что никакие обратные вызовы не могут быть запущены, когда мы вызываем Dispose.Я думал, что ReaderWriterLockSlim может быть уместным - вам нужна блокировка записи для вызова Dispose, а обратным вызовам нужны блокировки чтения для вызова Query.Проблема здесь в том, что ReaderWriterLockSlim является IDisposable, и я не думаю, что когда-либо будет безопасно утилизировать его - я никогда не могу знать, что в полете нет обратного вызова, который просто не дошел до точки получения чтениязаблокировать еще.

Что мне делать?Похоже, что ReaderWriterLock не IDisposable, но в собственной документации сказано, что вместо этого следует использовать ReaderWriterLockSlim.Я мог бы попытаться сделать что-то эквивалентное только с ключевым словом "lock", но это звучит расточительно и легко напортачить.

PS - Не стесняйтесь говорить, что API библиотеки не годится, если вы считаете, что это так,Я лично предпочел бы, чтобы это гарантировало, что Dispose будет блокироваться, пока все обратные вызовы не будут завершены.

Ответы [ 2 ]

2 голосов
/ 23 сентября 2010

Это довольно сложная проблема, если вы попытались ее решить самостоятельно.Шаблон, который я собираюсь здесь описать, использует класс CountdownEvent в качестве основного механизма синхронизации.Он доступен в .NET 4.0 или как часть загрузки Reactive Extensions для .NET 3.5.Этот класс является идеальным кандидатом для проблем в этом жанре, потому что:

  • он может поддерживать счет.
  • он может ждать, пока этот счет достигнет нуля.

Позвольте мне описать схему.Я создал класс 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();
        }

    }
}
2 голосов
/ 23 сентября 2010

Это звучит как то, что вы можете заключить в свой собственный API, который дает гарантию из последнего абзаца.

По сути, каждый обратный вызов должен атомарно регистрировать, что он работает, и проверять, все ли нормально для запуска - и затем либо немедленно завершать работу (эквивалентно тому, что он никогда не вызывался), либо выполнять свои действия и отменять регистрацию, что он работает.

Ваш Dispose метод просто должен блокироваться, пока не найдет время, когда ничего не запущено, атомарно проверяет, работает ли что-нибудь, и делает недействительным, если нет.

Я могу представить, что это делается разумно , просто используя простой замок, монитор, подход Wait / Pulse. Ваша оболочка API обернет любой обратный вызов, который он дал, в другой обратный вызов, который делает все это, поэтому вам нужно всего лишь поместить логику в одно место.

Вы понимаете, о чем я? У меня нет времени, чтобы реализовать это для вас прямо сейчас, но я могу развить идеи, если хотите.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...