Как я узнаю, когда можно позвонить в Dispose? - PullRequest
5 голосов
/ 16 сентября 2011

У меня есть поисковое приложение, которое занимает некоторое время (от 10 до 15 секунд), чтобы вернуть результаты для некоторых запросов. Нередко бывает несколько одновременных запросов на одну и ту же информацию. В настоящее время я должен обрабатывать их независимо, что приводит к ненужной обработке.

Я придумал дизайн, который позволил бы мне избежать ненужной обработки, но есть одна затянувшаяся проблема.

Каждый запрос имеет ключ, который идентифицирует запрашиваемые данные. Я веду словарь запросов, ключом которого является ключ запроса. Объект запроса имеет некоторую информацию о состоянии и WaitHandle, который используется для ожидания результатов.

Когда клиент вызывает мой метод Search, код проверяет словарь, чтобы узнать, существует ли уже запрос для этого ключа. Если это так, клиент просто ждет на WaitHandle. Если запрос не существует, я создаю его, добавляю в словарь и выполняю асинхронный вызов для получения информации. И снова код ожидает события.

Когда асинхронный процесс получил результаты, он обновляет объект запроса, удаляет запрос из словаря и затем сигнализирует о событии.

Все это прекрасно работает. За исключением того, что я не знаю, когда распоряжаться объектом запроса. То есть, поскольку я не знаю, когда его использует последний клиент, я не могу вызвать Dispose. Мне нужно подождать, пока придет сборщик мусора и вычистит.

Вот код:

class SearchRequest: IDisposable
{
    public readonly string RequestKey;
    public string Results { get; set; }
    public ManualResetEvent WaitEvent { get; private set; }

    public SearchRequest(string key)
    {
        RequestKey = key;
        WaitEvent = new ManualResetEvent(false);
    }

    public void Dispose()
    {
        WaitEvent.Dispose();
        GC.SuppressFinalize(this);
    }
}

ConcurrentDictionary<string, SearchRequest> Requests = new ConcurrentDictionary<string, SearchRequest>();

string Search(string key)
{
    SearchRequest req;
    bool addedNew = false;
    req = Requests.GetOrAdd(key, (s) =>
        {
            // Create a new request.
            var r = new SearchRequest(s);
            Console.WriteLine("Added new request with key {0}", key);
            addedNew = true;
            return r;
        });

    if (addedNew)
    {
        // A new request was created.
        // Start a search.
        ThreadPool.QueueUserWorkItem((obj) =>
            {
                // Get the results
                req.Results = DoSearch(req.RequestKey);  // DoSearch takes several seconds

                // Remove the request from the pending list
                SearchRequest trash;
                Requests.TryRemove(req.RequestKey, out trash);

                // And signal that the request is finished
                req.WaitEvent.Set();
            });
    }

    Console.WriteLine("Waiting for results from request with key {0}", key);
    req.WaitEvent.WaitOne();
    return req.Results;
}

По сути, я не знаю, когда будет выпущен последний клиент. Независимо от того, как я нарежу это здесь, у меня есть состояние гонки. Рассмотрим:

  1. Поток A Создает новый запрос, запускает Поток 2 и ожидает дескриптор ожидания.
  2. Поток B Начинает обработку запроса.
  3. Поток C обнаруживает, что есть ожидающий запрос, а затем выгружается.
  4. Тема B Завершает запрос, удаляет элемент из словаря и устанавливает событие.
  5. Ожидание потока А удовлетворено и возвращает результат.
  6. Поток C просыпается, вызывает WaitOne, освобождается и возвращает результат.

Если я использую какой-то счетчик ссылок, чтобы «последний» клиент вызывал Dispose, тогда объект был бы удален потоком A в приведенном выше сценарии. Поток C умрет, когда попытается ждать на удаленном WaitHandle.

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

Другим решением было бы отказаться от WaitHandle и использовать механизм, похожий на событие, с обратными вызовами. Но это также потребовало бы от меня защиты поисков с помощью блокировки, и у меня возникло дополнительное затруднение, связанное с событием или голым многоадресным делегатом. Это тоже похоже на взлом.

В настоящее время это, вероятно, не является проблемой, потому что это приложение еще не получает достаточно трафика, чтобы эти оставленные дескрипторы суммировались до того, как придет следующий проход GC и очистит их. А может быть, это никогда не будет проблемой? Меня беспокоит, однако, что я оставляю их для очистки GC, когда я должен позвонить Dispose, чтобы избавиться от них.

Идеи? Это потенциальная проблема? Если так, у вас есть чистый раствор?

Ответы [ 2 ]

4 голосов
/ 16 сентября 2011

Рассмотрите возможность использования Lazy<T> для SearchRequest.Results, может быть? Но это, вероятно, повлечет за собой небольшую переработку. Не подумал об этом полностью.

Но то, что, вероятно, было бы почти полной заменой для вашего варианта использования, это реализация ваших собственных Wait() и Set() методов в SearchRequest. Что-то вроде:

object _resultLock;

void Wait()
{
  lock(_resultLock)
  {
     while (!_hasResult)
       Monitor.Wait(_resultLock);
  }
}

void Set(string results)
{
  lock(_resultLock)
  {
     Results = results;
     _hasResult = true;
     Monitor.PulseAll(_resultLock);
  }
}

Нет необходимости избавляться. :)

2 голосов
/ 16 сентября 2011

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

Что касается моего комментария к вашему вопросу, вы должны иметь в виду, что ConcurrentDictionary имеет побочные эффекты. Если несколько потоков пытаются вызвать GetOrAdd одновременно, то фабрика может быть вызвана для всех из них, но победит только один. Значения, полученные для других потоков, будут просто отброшены, однако к тому времени вычисления будут завершены.

Так как вы также сказали, что выполнение поиска обходится дорого, тогда затраты на снятие блокировки рекламы при использовании стандартного словаря будут минимальными.

Так вот что я предлагаю:

private Dictionary<string, Task<string>> _requests
    = new Dictionary<string, Task<string>>();

public string Search(string key)
{
    Task<string> task;
    lock (_requests)
    {
        if (_requests.ContainsKey(key))
        {
            task = _requests[key];
        }
        else
        {
            task = Task<string>
                .Factory
                .StartNew(() => DoSearch(key));
            _requests[key] = task;
            task.ContinueWith(t =>
            {
                lock(_requests)
                {
                    _requests.Remove(key);
                }
            });
        }
    }
    return task.Result;
}

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

Я протестировал код, и он работает.

...