我怎么知道什么时候调用Dispose是安全的?

时间:2011-09-16 00:19:49

标签: c# multithreading dispose race-condition

我有一个搜索应用程序,需要一些时间(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. 线程A的等待已满足,并返回结果。
  6. 线程C唤醒,调用WaitOne,释放,并返回结果。
  7. 如果我使用某种引用计数以便“last”客户端调用Dispose,那么该对象将在上面的场景中由线程A处理。当线程C试图等待被处置的WaitHandle时,它就会死掉。

    我能看到修复此问题的唯一方法是使用引用计数方案并使用锁保护对字典的访问(在这种情况下使用ConcurrentDictionary是没有意义的),以便查找始终伴随着引用计数的增量。虽然这会起作用,但它似乎是一个丑陋的黑客。

    另一个解决方案是放弃WaitHandle并使用类似事件的机制进行回调。但是,这也需要我用锁来保护查找,并且我还有处理事件或裸组播委托的复杂性。这似乎也是一种黑客攻击。

    目前这可能不是问题,因为此应用程序尚未获得足够的流量,以便在下一次GC传递到来之前将这些废弃的句柄添加起来并清除它们。也许它永远不会成为一个问题?但是,让我担心的是,当我打电话给Dispose以摆脱它们时,我要让它们被GC清理干净。

    想法?这是一个潜在的问题吗?如果是这样,你有一个干净的解决方案吗?

2 个答案:

答案 0 :(得分:4)

考虑将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);
  }
}

无需处置。 :)

答案 1 :(得分:2)

我认为,最好的办法就是将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;
}

此选项很好地运行搜索,在整个搜索过程中记住任务,然后在完成时将其从字典中删除。执行搜索时对同一密钥的所有请求都会执行相同的任务,因此一旦任务完成,将获得相同的结果。

我测试了代码,但它确实有用。