É péssimo usar this
nas instruções de bloqueio, porque geralmente está fora de seu controle quem mais pode estar bloqueando esse objeto.
Para planejar adequadamente as operações paralelas, deve-se tomar cuidado especial para considerar possíveis situações de conflito, e ter um número desconhecido de pontos de entrada de bloqueio dificulta isso. Por exemplo, qualquer pessoa com uma referência ao objeto pode travá-lo sem que o criador / criador do objeto saiba disso. Isso aumenta a complexidade das soluções multithread e pode afetar sua correção.
Um campo privado geralmente é uma opção melhor, pois o compilador aplicará restrições de acesso a ele e encapsulará o mecanismo de bloqueio. O uso this
viola o encapsulamento, expondo parte da sua implementação de bloqueio ao público. Também não está claro que você estará adquirindo um bloqueio, a this
menos que tenha sido documentado. Mesmo assim, depender da documentação para evitar um problema é sub-ideal.
Finalmente, existe o equívoco comum que lock(this)
realmente modifica o objeto passado como parâmetro e, de alguma forma, o torna somente leitura ou inacessível. Isso é falso . O objeto passado como parâmetro lock
apenas serve como chave . Se uma fechadura já estiver sendo mantida nessa chave, a fechadura não poderá ser feita; caso contrário, o bloqueio é permitido.
É por isso que é ruim usar cadeias de caracteres como as chaves nas lock
instruções, pois elas são imutáveis e são compartilhadas / acessíveis em partes do aplicativo. Você deve usar uma variável privada em vez disso, uma Object
instância será bem.
Execute o seguinte código C # como um exemplo.
public class Person
{
public int Age { get; set; }
public string Name { get; set; }
public void LockThis()
{
lock (this)
{
System.Threading.Thread.Sleep(10000);
}
}
}
class Program
{
static void Main(string[] args)
{
var nancy = new Person {Name = "Nancy Drew", Age = 15};
var a = new Thread(nancy.LockThis);
a.Start();
var b = new Thread(Timewarp);
b.Start(nancy);
Thread.Sleep(10);
var anotherNancy = new Person { Name = "Nancy Drew", Age = 50 };
var c = new Thread(NameChange);
c.Start(anotherNancy);
a.Join();
Console.ReadLine();
}
static void Timewarp(object subject)
{
var person = subject as Person;
if (person == null) throw new ArgumentNullException("subject");
// A lock does not make the object read-only.
lock (person.Name)
{
while (person.Age <= 23)
{
// There will be a lock on 'person' due to the LockThis method running in another thread
if (Monitor.TryEnter(person, 10) == false)
{
Console.WriteLine("'this' person is locked!");
}
else Monitor.Exit(person);
person.Age++;
if(person.Age == 18)
{
// Changing the 'person.Name' value doesn't change the lock...
person.Name = "Nancy Smith";
}
Console.WriteLine("{0} is {1} years old.", person.Name, person.Age);
}
}
}
static void NameChange(object subject)
{
var person = subject as Person;
if (person == null) throw new ArgumentNullException("subject");
// You should avoid locking on strings, since they are immutable.
if (Monitor.TryEnter(person.Name, 30) == false)
{
Console.WriteLine("Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string \"Nancy Drew\".");
}
else Monitor.Exit(person.Name);
if (Monitor.TryEnter("Nancy Drew", 30) == false)
{
Console.WriteLine("Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!");
}
else Monitor.Exit("Nancy Drew");
if (Monitor.TryEnter(person.Name, 10000))
{
string oldName = person.Name;
person.Name = "Nancy Callahan";
Console.WriteLine("Name changed from '{0}' to '{1}'.", oldName, person.Name);
}
else Monitor.Exit(person.Name);
}
}
Saída do console
'this' person is locked!
Nancy Drew is 16 years old.
'this' person is locked!
Nancy Drew is 17 years old.
Failed to obtain lock on 50 year old Nancy, because Timewarp(object) locked on string "Nancy Drew".
'this' person is locked!
Nancy Smith is 18 years old.
'this' person is locked!
Nancy Smith is 19 years old.
'this' person is locked!
Nancy Smith is 20 years old.
Failed to obtain lock using 'Nancy Drew' literal, locked by 'person.Name' since both are the same object thanks to inlining!
'this' person is locked!
Nancy Smith is 21 years old.
'this' person is locked!
Nancy Smith is 22 years old.
'this' person is locked!
Nancy Smith is 23 years old.
'this' person is locked!
Nancy Smith is 24 years old.
Name changed from 'Nancy Drew' to 'Nancy Callahan'.
lock(this)
é um conselho padrão; é importante observar que isso geralmente impossibilita que o código externo faça com que o bloqueio associado ao objeto seja retido entre as chamadas de método. Isso pode ou não ser uma coisa boa . Existe algum perigo em permitir que códigos externos mantenham um bloqueio por duração arbitrária, e as classes geralmente devem ser projetadas para tornar esse uso desnecessário, mas nem sempre existem alternativas práticas. Como um exemplo simples, a menos que uma implementos coleta umaToArray
ouToList
método próprio ...there is the common misconception that lock(this) actually modifies the object passed as a parameter, and in some way makes it read-only or inaccessible. This is false
- Eu acredito que essas conversações são sobre SyncBlock mordeu no CLR objeto de modo formalmente isso é certo - lock objeto modificado em siPorque se as pessoas puderem acessar o
this
ponteiro da instância do seu objeto (ou seja: seu ), também poderão tentar bloquear o mesmo objeto. Agora eles podem não estar cientes de que você está travandothis
internamente, portanto isso pode causar problemas (possivelmente um impasse)Além disso, também é uma prática ruim, porque bloqueia "demais"
Por exemplo, você pode ter uma variável de membro de
List<int>
e a única coisa que realmente precisa bloquear é essa variável de membro. Se você bloquear o objeto inteiro em suas funções, outras coisas que chamam essas funções serão bloqueadas aguardando o bloqueio. Se essas funções não precisarem acessar a lista de membros, você estará fazendo com que outro código aguarde e desacelere seu aplicativo sem motivo algum.fonte
Dê uma olhada no tópico Sincronização de threads do MSDN (Guia de Programação em C #)
fonte
Eu sei que esse é um tópico antigo, mas como as pessoas ainda podem procurar e confiar nele, parece importante ressaltar que
lock(typeof(SomeObject))
é significativamente pior do quelock(this)
. Tendo dito isto; parabéns sinceros a Alan por apontar quelock(typeof(SomeObject))
é uma má prática.Uma instância de
System.Type
é um dos objetos mais genéricos e de granulação grossa que existe. No mínimo, uma instância do System.Type é global para um AppDomain e o .NET pode executar vários programas em um AppDomain. Isso significa que dois programas completamente diferentes podem potencialmente causar interferência um no outro, até o ponto de criar um impasse, se ambos tentarem obter um bloqueio de sincronização na mesma instância de tipo.Portanto,
lock(this)
não é uma forma particularmente robusta, pode causar problemas e deve sempre levantar as sobrancelhas por todos os motivos citados. No entanto, existe um código amplamente usado, relativamente respeitado e aparentemente estável, como o log4net, que usa extensivamente o padrão lock (este), mesmo que eu pessoalmente prefira ver esse padrão mudar.Mas
lock(typeof(SomeObject))
abre uma lata de vermes totalmente nova e aprimorada.Pelo que vale a pena.
fonte
... e exatamente os mesmos argumentos se aplicam a essa construção também:
fonte
lock(this)
pareça particularmente lógico e sucinto. É um bloqueio muito grosseiro, e qualquer outro código pode bloquear o seu objeto, causando interferência no seu código interno. Faça bloqueios mais granulares e assuma um controle mais rígido. Olock(this)
que tem para isso é que é muito melhor do quelock(typeof(SomeObject))
.Imagine que você tenha uma secretária qualificada em seu escritório que seja um recurso compartilhado no departamento. De vez em quando, você corre na direção deles porque tem uma tarefa, apenas esperando que outro colega de trabalho ainda não os tenha reivindicado. Normalmente você só precisa esperar um breve período de tempo.
Como cuidar é compartilhar, seu gerente decide que os clientes também podem usar a secretária diretamente. Mas isso tem um efeito colateral: um cliente pode até reivindicá-los enquanto você trabalha para esse cliente e também é necessário que ele execute parte das tarefas. Um impasse ocorre porque a reivindicação não é mais uma hierarquia. Isso poderia ter sido evitado em conjunto, não permitindo que os clientes os reivindiquem em primeiro lugar.
lock(this)
é ruim como vimos. Um objeto externo pode travar no objeto e, como você não controla quem está usando a classe, qualquer pessoa pode travá-lo ... Qual é o exemplo exato, conforme descrito acima. Mais uma vez, a solução é limitar a exposição do objeto. No entanto, se você tem uma classeprivate
,protected
ou já pode controlar quem está bloqueando seu objeto , porque você tem certeza de que escreveu o seu código. Portanto, a mensagem aqui é: não a exponha como . Além disso, garantir que um bloqueio seja usado em cenários semelhantes evita conflitos.internal
public
O oposto completo disso é bloquear recursos compartilhados em todo o domínio do aplicativo - o pior cenário. É como colocar sua secretária do lado de fora e permitir que todos lá fora os reivindiquem. O resultado é um caos absoluto - ou em termos de código fonte: foi uma má idéia; jogue fora e comece de novo. Então, como fazemos isso?
Os tipos são compartilhados no domínio do aplicativo, como a maioria das pessoas indica. Mas há coisas ainda melhores que podemos usar: cordas. A razão é que as cadeias são agrupadas . Em outras palavras: se você tiver duas cadeias de caracteres com o mesmo conteúdo em um domínio de aplicativo, é possível que elas tenham exatamente o mesmo ponteiro. Como o ponteiro é usado como chave de bloqueio, o que você basicamente obtém é sinônimo de "preparar-se para um comportamento indefinido".
Da mesma forma, você não deve bloquear objetos WCF, HttpContext.Current, Thread.Current, Singletons (em geral) etc. A maneira mais fácil de evitar tudo isso?
private [static] object myLock = new object();
fonte
O bloqueio desse ponteiro pode ser ruim se você estiver bloqueando um recurso compartilhado . Um recurso compartilhado pode ser uma variável estática ou um arquivo no seu computador - ou seja, algo que é compartilhado entre todos os usuários da classe. O motivo é que o ponteiro this conterá uma referência diferente para um local na memória cada vez que sua classe for instanciada. Portanto, bloquear isso em uma instância de classe é diferente de bloquear isso em outra instância de uma classe.
Confira este código para ver o que quero dizer. Adicione o seguinte código ao seu programa principal em um aplicativo de console:
Crie uma nova classe como a abaixo.
Aqui está uma execução do programa bloqueando isso .
Aqui está uma execução do programa bloqueando no myLock .
fonte
Random rand = new Random();
NVM, acho que vejo o equilíbrio repetidoHá um artigo muito bom sobre isso http://bytes.com/topic/c-sharp/answers/249277-dont-lock-type-objects por Rico Mariani, arquiteto de desempenho para o tempo de execução do Microsoft® .NET
Excerto:
fonte
Também há uma boa discussão sobre isso aqui: esse é o uso adequado de um mutex?
fonte
Aqui está uma ilustração muito mais simples (tirada da pergunta 34 aqui ) por que o bloqueio (isso) é ruim e pode resultar em conflitos quando o consumidor da sua classe também tenta bloquear o objeto. Abaixo, apenas um dos três segmentos pode prosseguir, os outros dois estão em um impasse.
Para contornar, esse cara usou o Thread.TryMonitor (com tempo limite) em vez do bloqueio:
https://blogs.appbeat.io/post/c-how-to-lock-without-deadlocks
fonte
SomeClass
, ainda recebo o mesmo impasse. Além disso, se o bloqueio na classe principal for feito em outro membro da Instância particular do Programa, o mesmo bloqueio ocorrerá. Portanto, não tenho certeza se essa resposta não é enganosa e incorreta. Veja esse comportamento aqui: dotnetfiddle.net/DMrU5hPorque qualquer parte do código que pode ver a instância da sua classe também pode bloquear essa referência. Você deseja ocultar (encapsular) seu objeto de bloqueio para que apenas o código que precise fazer referência a ele possa fazer referência a ele. A palavra-chave refere-se à instância atual da classe, portanto, várias coisas podem ter referência a ela e usá-la para sincronizar threads.
Para ficar claro, isso é ruim porque outro pedaço de código pode usar a instância da classe para bloquear e pode impedir que seu código obtenha um bloqueio oportuno ou criar outros problemas de sincronização de encadeamento. Melhor caso: nada mais usa uma referência à sua classe para bloquear. Caso intermediário: algo usa uma referência à sua classe para fazer bloqueios e causa problemas de desempenho. Pior: algo usa uma referência da sua classe para fazer bloqueios e causa problemas muito ruins, sutis e difíceis de depurar.
fonte
Desculpe pessoal, mas não posso concordar com o argumento de que bloquear isso pode causar um impasse. Você está confundindo duas coisas: impasse e fome.
Aqui está uma imagem que ilustra a diferença.
Conclusão
Você ainda pode usar com segurança
lock(this)
se a falta de thread não for um problema para você. Você ainda deve ter em mente que, quando o thread, que está passando fome usando aslock(this)
extremidades de uma fechadura e com o objeto bloqueado, finalmente termina em inanição eterna;)fonte
lock(this)
- esse tipo de código está simplesmente errado. Eu apenas acho que chamar de impasse é um pouco abusivo.Consulte o link a seguir, que explica por que bloquear (isso) não é uma boa ideia.
http://blogs.msdn.com/b/bclteam/archive/2004/01/20/60719.aspx
Portanto, a solução é adicionar um objeto privado, por exemplo, lockObject à classe e colocar a região de código dentro da instrução lock, como mostrado abaixo:
fonte
Aqui está um código de exemplo mais simples de seguir (IMO): ( Funcionará no LinqPad , faça referência aos seguintes namespaces: System.Net e System.Threading.Tasks)
Algo a lembrar é que o bloqueio (x) é basicamente um açúcar sintático e o que ele faz é usar o Monitor.Enter e, em seguida, usa um bloco try, catch, finalmente chamado Call.Exit. Consulte: https://docs.microsoft.com/en-us/dotnet/api/system.threading.monitor.enter (seção de comentários)
Resultado
Observe que o segmento nº 12 nunca termina, pois está totalmente bloqueado.
fonte
DoWorkUsingThisLock
segmento não é necessário para ilustrar o problema?Você pode estabelecer uma regra que diz que uma classe pode ter um código que bloqueie 'this' ou qualquer objeto que o código da classe instancia. Portanto, é apenas um problema se o padrão não for seguido.
Se você deseja se proteger de um código que não segue esse padrão, a resposta aceita está correta. Mas se o padrão for seguido, não será um problema.
A vantagem do bloqueio (isso) é a eficiência. E se você tiver um "objeto de valor" simples que contém um único valor. É apenas um invólucro e é instanciado milhões de vezes. Ao exigir a criação de um objeto de sincronização privado apenas para o bloqueio, você basicamente duplicou o tamanho do objeto e o número de alocações. Quando o desempenho importa, isso é uma vantagem.
Quando você não se importa com o número de alocações ou espaço ocupado na memória, é preferível evitar o bloqueio (isso) pelos motivos indicados em outras respostas.
fonte
Haverá um problema se a instância puder ser acessada publicamente porque pode haver outras solicitações que podem estar usando a mesma instância de objeto. É melhor usar variável privada / estática.
fonte