Pergunta

Tenho uma aplicação ASP.NET com um monte de conteúdo dinâmico. O conteúdo é o mesmo para todos os usuários pertencentes a um cliente particular. Para reduzir o número de visitas de banco de dados necessárias por solicitação, decidi dados em nível de cliente de cache. Eu criei uma classe estática ( "ClientCache") para armazenar os dados.
O método mais frequentemente utilizado da classe é de longe "GetClientData", que traz de volta um objeto ClientData contendo todos os dados armazenados para um cliente particular. ClientData é carregado preguiçosamente, no entanto: se os dados do cliente solicitado já está em cache, o chamador recebe os dados em cache; caso contrário, os dados são buscados, adicionado ao cache e depois voltou para o chamador.

Finalmente eu comecei a ficar bloqueios intermitentes no método GetClientData na linha onde o objeto ClientData é adicionado ao cache. Aqui está o corpo do método:

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;
}

O texto exceção é sempre algo como "Um objeto com a mesma chave já existe." Claro, eu tentei escrever o código para que ele simplesmente não era possível adicionar um cliente para o cache se ele já existia.

Neste ponto, estou suspeitando que eu tenho uma condição de corrida e o método está sendo executado duas vezes simultaneamente, o que poderia explicar como o código iria falhar. O que eu estou confuso sobre, porém, é como o método pode ser executado duas vezes ao mesmo tempo em tudo. Tanto quanto eu sei, qualquer aplicação ASP.NET sempre apenas campos um pedido de cada vez (é por isso que nós podemos usar HttpContext.Current).

Assim, é este bug provavelmente uma condição de corrida que vai exigir colocar fechaduras em seções críticas? Ou estou faltando um erro mais óbvio?

Foi útil?

Solução

Se uma aplicação ASP.NET lida apenas com um pedido de cada vez todos os sites ASP.NET estaria em sérios apuros. ASP.NET pode processar dezenas em um tempo (normalmente 25 por núcleo de CPU).

Você deve usar ASP.NET Cache em vez de usar o seu próprio dicionário para armazenar o objeto. Operações em cache são thread-safe.

Note que você precisa ter certeza de que operação de leitura no objeto que você armazenar em cache são threadsafe, infelizmente a maioria da classe .NET simplesmente indicar os membros de instância não são threadsafe sem tentar apontar qualquer que seja.

Editar :

Um comentário para esta resposta estados: -

Apenas operações atômicas sobre o cache são segmento seguro. Se você fizer algo como verificação
se uma chave existe e, em seguida, adicioná-lo, isso não é thread-safe e pode causar o item para
substituído.

Vale a pena ressaltar que se nós sentimos que precisamos fazer tal operação atômica então o cache não é provavelmente o lugar certo para o recurso.

Eu tenho um pouco de código que faz exatamente como o comentário descreve. No entanto, o recurso que está sendo armazenado será o mesmo em ambos os lugares. Portanto, se um item existente em raras ocasiões, ele será substituído o único custo é que um segmento desnecessariamente gerado um recurso. O custo deste evento raro é muito menos do que o custo de tentar fazer a operação atômica cada vez que uma tentativa de acesso é feita.

Outras dicas

Isto é muito fácil de resolver:

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;
  }
}

Tenha em mente que sempre que você mexe com classes estáticas, você tem o potencial para problemas de sincronização de segmento. Se há uma lista de nível de classe estática de algum tipo (neste caso, _clients, o objeto Dictionary), há DEFINITIVAMENTE vai ser problemas de sincronização thread para lidar com eles.

O seu código realmente faz supor apenas um thread está na função de cada vez.

Esta simplesmente não ser verdade em ASP.NET

Se você insistir em fazê-lo desta forma, usar um semáforo estática para bloquear a área em torno desta classe.

Você precisa o segmento de seguros e minimizar bloqueio.
veja bloqueio duplo verificado ( http://en.wikipedia.org/wiki/Double-checked_locking )

gravar simplesmente com 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;
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top