我有一个搜索应用程序,对于某些请求,返回结果需要一定的时间(10到15秒)。同样的请求可能同时拥有多个并发请求。目前情况下,我必须独立处理这些请求,这会导致大量不必要的处理。
我已经设计了一个方案,应该可以避免不必要的处理,但还有一个悬而未决的问题。
每个请求都有一个标识所请求的数据的键。我维护一个由请求组成的字典,用请求的键进行索引。请求对象具有一些状态信息和用于等待结果的 WaitHandle。
当客户端调用我的 Search 方法时,代码会检查字典,看看是否已经存在该键的请求。如果是,则客户端只需等待 WaitHandle。如果没有请求存在,我就创建一个请求,将其添加到字典中,并发出异步调用以获取信息。同样地,代码会等待事件。
当异步进程获取结果时,它会更新请求对象,从字典中删除请求,然后发出信号。
这一切都很顺利。除了我不知道何时处理请求对象。也就是说,由于我不知道最后一个客户端何时使用它,因此无法调用 Dispose。我必须等待垃圾收集器来清理。
以下是代码:
基本上,我不知道最后一个客户端将何时发布。无论我怎样分析这里的情况,都存在竞争条件。考虑以下情况:
我能想到的唯一解决方法是使用引用计数方案,并使用锁保护对字典的访问(在这种情况下使用
另一种解决方案是放弃
目前可能不是问题,因为该应用程序尚未获得足够的流量,以使那些被遗弃的句柄在下一次GC通过之前累积起来,然后进行清理。也许它永远不会成为问题?但是如果我应该调用
有什么想法吗?这是一个潜在的问题吗?如果是,您有一个干净的解决方案吗?
我已经设计了一个方案,应该可以避免不必要的处理,但还有一个悬而未决的问题。
每个请求都有一个标识所请求的数据的键。我维护一个由请求组成的字典,用请求的键进行索引。请求对象具有一些状态信息和用于等待结果的 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完成请求,从字典中删除该项,并设置事件。
- 线程A的等待得到满足,返回结果。
- 线程C唤醒,调用
WaitOne
,被释放并返回结果。
Dispose
,那么在上述情况下对象将由线程A进行处理。然后当线程C尝试等待已释放的WaitHandle
时,它就会死掉。我能想到的唯一解决方法是使用引用计数方案,并使用锁保护对字典的访问(在这种情况下使用
ConcurrentDictionary
是没有意义的),以便每次查找都伴随着引用计数的增加。虽然这样做可以解决问题,但看起来很丑陋。另一种解决方案是放弃
WaitHandle
,使用类似事件的机制和回调。但这也需要我用锁保护查找,并且我还要处理事件或裸多播委托的额外复杂性。这也似乎是一个hack。目前可能不是问题,因为该应用程序尚未获得足够的流量,以使那些被遗弃的句柄在下一次GC通过之前累积起来,然后进行清理。也许它永远不会成为问题?但是如果我应该调用
Dispose
来摆脱它们,而不是将它们留给GC来清理,这让我感到担忧。有什么想法吗?这是一个潜在的问题吗?如果是,您有一个干净的解决方案吗?
ConcurrentDictionary
具有副作用。如果多个线程尝试同时调用GetOrAdd
,则工厂可能会被调用多次,但只有一个线程可以成功添加。为其他线程生成的值将被丢弃,但此时计算已完成。 - Enigmativity.Dispose()
- 您始终需要在代码中显式地调用.Dispose()
。 - EnigmativityConcurrentDictionary
会针对相同的键在多个线程上调用工厂方法,这让我有点失望。我一定要重新审视解决方案的这部分。谢谢。 - Jim Mischel