У меня есть поисковое приложение, которое занимает некоторое время (от 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;
}
По сути, я не знаю, когда будет выпущен последний клиент. Независимо от того, как я нарежу это здесь, у меня есть состояние гонки. Рассмотрим:
- Поток A Создает новый запрос, запускает Поток 2 и ожидает дескриптор ожидания.
- Поток B Начинает обработку запроса.
- Поток C обнаруживает, что есть ожидающий запрос, а затем выгружается.
- Тема B Завершает запрос, удаляет элемент из словаря и устанавливает событие.
- Ожидание потока А удовлетворено и возвращает результат.
- Поток C просыпается, вызывает
WaitOne
, освобождается и возвращает результат.
Если я использую какой-то счетчик ссылок, чтобы «последний» клиент вызывал Dispose
, тогда объект был бы удален потоком A в приведенном выше сценарии. Поток C умрет, когда попытается ждать на удаленном WaitHandle
.
Единственный способ исправить это - использовать схему подсчета ссылок и защитить доступ к словарю с помощью блокировки (в этом случае использование ConcurrentDictionary
бессмысленно), так что поиск всегда сопровождается приращением счетчик ссылок. Принимая во внимание, что это сработает, это похоже на уродливый хак.
Другим решением было бы отказаться от WaitHandle
и использовать механизм, похожий на событие, с обратными вызовами. Но это также потребовало бы от меня защиты поисков с помощью блокировки, и у меня возникло дополнительное затруднение, связанное с событием или голым многоадресным делегатом. Это тоже похоже на взлом.
В настоящее время это, вероятно, не является проблемой, потому что это приложение еще не получает достаточно трафика, чтобы эти оставленные дескрипторы суммировались до того, как придет следующий проход GC и очистит их. А может быть, это никогда не будет проблемой? Меня беспокоит, однако, что я оставляю их для очистки GC, когда я должен позвонить Dispose
, чтобы избавиться от них.
Идеи? Это потенциальная проблема? Если так, у вас есть чистый раствор?