1

我有一个包含大量动态内容的 ASP.NET 应用程序。属于特定客户端的所有用户的内容都是相同的。为了减少每个请求所需的数据库命中次数,我决定缓存客户端级数据。我创建了一个静态类(“ClientCache”)来保存数据。
迄今为止,该类最常用的方法是“GetClientData”,它返回一个 ClientData 对象,其中包含特定客户端的所有存储数据。但是,ClientData 是延迟加载的:如果请求的客户端数据已经缓存,则调用者获取缓存的数据;否则,获取数据,添加到缓存中,然后返回给调用者。

最终,我开始在将 ClientData 对象添加到缓存的行的 GetClientData 方法中出现间歇性崩溃。这是方法体:

public static ClientData GetClientData(Guid fk_client)
{
    if (_clients == null)
        _clients = new Dictionary<Guid, ClientData>();

    ClientData client;
    if (_clients.ContainsKey(fk_client))
    {
        client = _clients[fk_client];
    }
    else
    {
        client = new ClientData(fk_client);
        _clients.Add(fk_client, client);
    }
    return client;
}

异常文本总是类似于“已存在具有相同键的对象”。当然,我尝试编写代码,以便如果客户端已经存在,则无法将其添加到缓存中。

在这一点上,我怀疑我有一个竞争条件并且该方法同时执行了两次,这可以解释代码是如何崩溃的。但是,我感到困惑的是,该方法如何可以同时执行两次。据我所知,任何 ASP.NET 应用程序一次只能处理一个请求(这就是我们可以使用 HttpContext.Current 的原因)。

那么,这个错误是否可能是一个需要在关键部分加锁的竞争条件?还是我错过了一个更明显的错误?

4

4 回答 4

3

如果一个 ASP.NET 应用程序一次只处理一个请求,那么所有 ASP.NET 站点都会遇到严重的问题。ASP.NET 一次可以处理数十个(通常每个 CPU 内核 25 个)。

您应该使用 ASP.NET 缓存而不是使用您自己的字典来存储您的对象。对缓存的操作是线程安全的。

请注意,您需要确保对存储在缓存中的对象的读取操作是线程安全的,不幸的是,大多数 .NET 类只是声明实例成员不是线程安全的,而没有尝试指出任何可能是线程安全的。

编辑

对此答案的评论指出:-

只有缓存上的原子操作是线程安全的。如果您执行诸如检查
密钥是否存在然后添加它之类的操作,这不是线程安全的,并且可能导致该项目
被覆盖。

值得指出的是,如果我们觉得我们需要使这样的操作原子化,那么缓存可能不是资源的正确位置。

我有相当多的代码完全按照评论的描述。但是,在这两个地方存储的资源将是相同的。因此,如果一个现有项目在极少数情况下被覆盖,唯一的代价是一个线程不必要地生成了一个资源。这种罕见事件的成本远低于每次尝试访问它时尝试使操作原子化的成本。

于 2009-02-21T21:53:33.010 回答
2

这很容易解决:

private _clientsLock = new Object();

public static ClientData GetClientData(Guid fk_client)
{
  if (_clients == null)
    lock (_clientsLock)
      // Check again because another thread could have created a new 
      // dictionary in-between the lock and this check
      if (_clients == null) 
        _clients = new Dictionary<Guid, ClientData>();

  if (_clients.ContainsKey(fk_client))
    // Don't need a lock here UNLESS there are also deletes. If there are
    // deletes, then a lock like the one below (in the else) is necessary
    return _clients[fk_client];
  else
  {
    ClientData client = new ClientData(fk_client);

    lock (_clientsLock)
      // Again, check again because another thread could have added this
      // this ClientData between the last ContainsKey check and this add
      if (!clients.ContainsKey(fk_client))
       _clients.Add(fk_client, client);

    return client;
  }
}

请记住,每当您使用静态类时,都可能出现线程同步问题。如果有某种类型的静态类级列表(在这种情况下,_clients,Dictionary对象),肯定会有线程同步问题需要处理。

于 2009-02-22T01:10:25.567 回答
0

您的代码确实假设一次只有一个线程在函数中。

这在 ASP.NET 中根本就不是真的

如果您坚持这样做,请使用静态信号量来锁定此类周围的区域。

于 2009-02-21T22:02:34.550 回答
0

你需要线程安全和最小化锁。
请参阅双重检查锁定(http://en.wikipedia.org/wiki/Double-checked_locking

简单地用 TryGetValue 编写。


public static object lockClientsSingleton = new object();

public static ClientData GetClientData(Guid fk_client)
{
    if (_clients == null) {
        lock( lockClientsSingleton ) {
            if( _clients==null ) {
                _clients = new Dictionary``();
            }
        }
    }
    ClientData client;
    if( !_clients.TryGetValue( fk_client, out client ) )
    {
        lock(_clients) 
        {
            if( !_clients.TryGetValue( fk_client, out client ) ) 
            {
                client = new ClientData(fk_client)
                _clients.Add( fk_client, client );
            }
        }
    }
    return client;
}
于 2009-10-19T02:48:22.460 回答