Um objeto bloqueado permanece bloqueado se ocorrer uma exceção dentro dele?

87

Em um aplicativo de encadeamento c #, se eu bloquear um objeto, digamos uma fila, e se ocorrer uma exceção, o objeto permanecerá bloqueado? Aqui está o pseudocódigo:

int ii;
lock(MyQueue)
{
   MyClass LclClass = (MyClass)MyQueue.Dequeue();
   try
   {
      ii = int.parse(LclClass.SomeString);
   }
   catch
   {
     MessageBox.Show("Error parsing string");
   }
}

Pelo que entendi, o código após a captura não é executado - mas estou me perguntando se o bloqueio será liberado.

Khadaji
fonte
1
Como um pensamento final (ver atualizações) - você provavelmente só deve segurar o bloqueio durante o desenfileiramento ... faça o processamento fora do bloqueio.
Marc Gravell
1
O código após a captura é executado porque a exceção foi tratada
cjk
Obrigado, devo ter esquecido essa, devo excluir esta pergunta?
Vort3x
4
Parece que o código de amostra não é bom para esta pergunta, mas a pergunta é bastante válida.
Salvador
Por C # Designer - Lock & Exception
Jivan

Respostas:

91

Primeiro; você considerou o TryParse?

in li;
if(int.TryParse(LclClass.SomeString, out li)) {
    // li is now assigned
} else {
    // input string is dodgy
}

O bloqueio será liberado por 2 motivos; primeiro, locké essencialmente:

Monitor.Enter(lockObj);
try {
  // ...
} finally {
    Monitor.Exit(lockObj);
}

Segundo; você captura e não re-lança a exceção interna, de modo que locknunca realmente veja uma exceção. Claro, você está segurando o bloqueio durante a MessageBox, o que pode ser um problema.

Portanto, ele será lançado em todas as exceções irrecuperáveis, exceto nas mais fatais e catastróficas.

Marc Gravell
fonte
15
Estou ciente do tryparse, mas não é verdadeiramente relevante para minha pergunta. Este era um código simples para explicar a questão - não uma verdadeira preocupação em relação à análise. Substitua a análise por qualquer código que force a captura e deixe você confortável.
Khadaji
13
Que tal lançar uma nova exceção ("para fins ilustrativos"); ;-p
Marc Gravell
1
Exceto se TheadAbortExceptionocorrer um entre Monitor.Entere try: blogs.msdn.com/ericlippert/archive/2009/03/06/…
Igal Tabachnik
2
"exceções irrecuperáveis ​​catastróficas fatais", como cruzar os riachos.
Phil Cooper
99

Observo que ninguém mencionou em suas respostas a esta velha questão que liberar um bloqueio em uma exceção é uma coisa incrivelmente perigosa de se fazer. Sim, as instruções de bloqueio em C # têm semântica "finalmente"; quando o controle sai do bloqueio normal ou anormalmente, o bloqueio é liberado. Vocês todos estão falando sobre isso como se fosse uma coisa boa, mas é uma coisa ruim! A coisa certa a fazer se você tiver uma região bloqueada que lança uma exceção não tratada é encerrar o processo doente imediatamente antes que destrua mais dados do usuário , e não liberar o bloqueio e continuar .

Veja desta forma: suponha que você tenha um banheiro com uma fechadura na porta e uma fila de pessoas esperando do lado de fora. Uma bomba explodiu no banheiro, matando a pessoa lá dentro. Sua pergunta é "nessa situação, a fechadura será desbloqueada automaticamente para que a próxima pessoa possa entrar no banheiro?" Sim vai. Isso não é uma coisa boa. Uma bomba explodiu lá e matou alguém! O encanamento provavelmente está destruído, a casa não é mais estruturalmente sólida e pode haver outra bomba lá . A coisa certa a fazer é tirar todos o mais rápido possível e demolir a casa inteira.

Quero dizer, pense bem: se você bloqueou uma região do código para ler de uma estrutura de dados sem sofrer mutação em outro encadeamento e algo nessa estrutura de dados lançou uma exceção, as chances são boas de que é porque a estrutura de dados está corrompido . Os dados do usuário agora estão confusos; você não deseja tentar salvar os dados do usuário neste momento porque, então, estará salvando dados corrompidos . Basta encerrar o processo.

Se você bloqueou uma região do código para realizar uma mutação sem outro thread lendo o estado ao mesmo tempo, e a mutação é lançada, então se os dados não estavam corrompidos antes, com certeza está agora . Qual é exatamente o cenário contra o qual a fechadura deve proteger . Agora, o código que está esperando para ler esse estado receberá imediatamente acesso ao estado corrompido e provavelmente travará. Novamente, a coisa certa a fazer é encerrar o processo.

Não importa como você o faça, uma exceção dentro de uma fechadura é uma má notícia . A pergunta certa a fazer não é "meu bloqueio será limpo no caso de uma exceção?" A pergunta certa a se fazer é "como posso garantir que nunca haja uma exceção dentro de um bloqueio? E, se houver, como faço para estruturar meu programa para que as mutações sejam revertidas para o estado normal anterior?"

Eric Lippert
fonte
19
Esse problema é bastante ortogonal ao bloqueio da IMO. Se você obtiver uma exceção esperada, deseja limpar tudo, incluindo bloqueios. E se você receber uma exceção inesperada, você tem um problema, com ou sem bloqueios.
CodesInChaos
10
Acho que a situação descrita acima é um generalismo. Às vezes, as exceções descrevem eventos catastróficos. Às vezes, não. Cada um os usa de maneira diferente no código. É perfeitamente válido que uma exceção seja um sinal de um evento excepcional, mas não catastrófico - assumindo exceções = catastrófico, o caso de encerramento do processo é muito específico. O fato de que pode ser um evento catastrófico não remove a validade da pergunta - a mesma linha de pensamento poderia levar você a nunca lidar com nenhuma exceção, caso em que o processo será encerrado ...
Gerasimos R
1
@GerasimosR: Certamente. Dois pontos a enfatizar. Em primeiro lugar, as exceções devem ser consideradas catastróficas até que sejam determinadas como benignas. Em segundo lugar, se você estiver recebendo uma exceção benigna lançada de uma região bloqueada, a região bloqueada provavelmente está mal projetada; provavelmente está trabalhando demais dentro da fechadura.
Eric Lippert
1
Com todo o respeito, mas parece que você está dizendo, Sr. Lippert, que o design C # do bloco de bloqueio, que por baixo do capô, emprega uma instrução finally, é uma coisa ruim. Em vez disso, devemos travar completamente todos os aplicativos que já tenham uma exceção em uma instrução de bloqueio que não foi capturada dentro do bloqueio. Talvez não seja isso que você está dizendo, mas se for assim, estou muito feliz que a equipe tenha seguido o caminho que fez e não o seu (extremista por motivos puristas!). Todo o respeito.
Nicholas Petersen
2
Caso não esteja claro o que quero dizer com não composível: suponha que temos um método "transferir" que pega duas listas, s e d, bloqueia s, bloqueia d, remove um item de s, adiciona o item a d, desbloqueia d, desbloqueia s. O método só é correta se ninguém nunca tentativas de transferência da lista X à lista Y ao mesmo tempo como alguém tentativas de transferência de Y para X . A exatidão do método de transferência não permite que você construa uma solução correta para um problema maior a partir dele, porque os bloqueios são mutações inseguras do estado global. Para "transferir" com segurança, você deve saber sobre cada bloqueio no programa.
Eric Lippert
43

sim, isso vai liberar corretamente; lockatua como try/ finally, com o Monitor.Exit(myLock)em finalmente, portanto, não importa como você saia, ele será liberado. Como uma observação lateral, catch(... e) {throw e;}é melhor evitar, pois isso danifica o rastreamento da pilha e; é melhor não pegá-lo de jeito nenhum , ou alternativamente: use em throw;vez de throw e;fazer um relance.

Se você realmente deseja saber, um bloqueio em C # 4 / .NET 4 é:

{
    bool haveLock = false;
    try {
       Monitor.Enter(myLock, ref haveLock);
    } finally {
       if(haveLock) Monitor.Exit(myLock);
    }
} 
Marc Gravell
fonte
14

"Uma instrução de bloqueio é compilada para uma chamada para Monitor.Enter e, em seguida, um bloco try… finally. No bloco finally, Monitor.Exit é chamado.

A geração de código JIT para x86 e x64 garante que um encadeamento abortado não ocorra entre uma chamada Monitor.Enter e um bloco try que a segue imediatamente. "

Retirado de: Este site

GH.
fonte
1
Há pelo menos um caso em que isso não é verdade: aborto de thread no modo de depuração em versões .net anteriores a 4. O motivo é que o compilador C # insere um NOP entre o Monitor.Entere o try, de modo que a condição "segue imediatamente" do JIT é violado.
CodesInChaos
6

Seu bloqueio será liberado corretamente. A lockage assim:

try {
    Monitor.Enter(myLock);
    // ...
} finally {
    Monitor.Exit(myLock);
}

E finallya execução dos blocos é garantida, não importa como você saia do trybloco.

Ry-
fonte
Na verdade, nenhum código é "garantido" para execução (você pode puxar o cabo de alimentação, por exemplo), e isso não é bem o que uma fechadura se parece no 4.0 - veja aqui
Marc Gravell
1
@MarcGravell: Pensei em colocar duas notas de rodapé sobre esses dois pontos exatos. E então eu percebi que não importaria muito :)
Ry-
1
@MarcGravel: Acho que todo mundo sempre assume que não está falando sobre uma situação de 'puxar a tomada', já que não é algo sobre o qual um programador tenha controle :)
Vort3x
5

Só para acrescentar um pouco à excelente resposta de Marc.

Situações como essa são a própria razão da existência da lockpalavra - chave. Isso ajuda os desenvolvedores a garantir que o bloqueio seja liberado no finallybloco.

Se você for forçado a usar Monitor.Enter/ Exitpor exemplo, para suportar um tempo limite, certifique-se de colocar a chamada para Monitor.Exitno finallybloco para garantir a liberação adequada do bloqueio em caso de uma exceção.

Brian Rasmussen
fonte