Quais são as diferenças entre as várias opções de sincronização de encadeamento em C #?

164

Alguém pode explicar a diferença entre:

  • lock (someobject) {}
  • Usando o Mutex
  • Usando o Semáforo
  • Usando o Monitor
  • Usando outras classes de sincronização .Net

Eu simplesmente não consigo entender. Parece-me que os dois primeiros são iguais?

user38834
fonte
Este link me ajudou muito: albahari.com/threading
Raphael

Respostas:

135

Ótima pergunta. Talvez eu esteja errado .. Deixe-me tentar .. Revisão 2 da minha resposta original .. com um pouco mais de compreensão. Obrigado por me fazer ler :)

bloqueio (obj)

  • é uma construção CLR que para sincronização de thread (intra-objeto?). Garante que apenas um segmento possa se apropriar do bloqueio do objeto e inserir o bloco de código bloqueado. Outros encadeamentos devem esperar até que o proprietário atual abandone o bloqueio saindo do bloco de código. Também é recomendável que você bloqueie um objeto membro privado da sua classe.

Monitores

  • O bloqueio (obj) é implementado internamente usando um Monitor. Você deve preferir o bloqueio (obj), pois impede que você se engane, como esquecer o procedimento de limpeza. É à prova de idiotas a construção Monitor, se você preferir.
    O uso do Monitor geralmente é preferido em relação aos mutexes, porque os monitores foram projetados especificamente para o .NET Framework e, portanto, fazem melhor uso dos recursos.

O uso de um bloqueio ou monitor é útil para impedir a execução simultânea de blocos de código sensíveis ao encadeamento, mas essas construções não permitem que um encadeamento comunique um evento a outro. Isso requer eventos de sincronização , que são objetos que possuem um dos dois estados, sinalizados e não sinalizados, que podem ser usados ​​para ativar e suspender encadeamentos. Mutex, semáforos são conceitos no nível do SO. por exemplo, com um mutex nomeado, você pode sincronizar vários exes (gerenciados) (garantindo que apenas uma instância do seu aplicativo esteja em execução na máquina).

Mutex:

  • Ao contrário dos monitores, no entanto, um mutex pode ser usado para sincronizar threads entre processos. Quando usado para sincronização entre processos, um mutex é chamado de mutex nomeado porque deve ser usado em outro aplicativo e, portanto, não pode ser compartilhado por meio de uma variável global ou estática. Ele deve receber um nome para que os dois aplicativos possam acessar o mesmo objeto mutex. Por outro lado, a classe Mutex é um invólucro para uma construção Win32. Embora seja mais poderoso que um monitor, um mutex exige transições de interoperabilidade que são mais caras em termos computacionais do que aquelas exigidas pela classe Monitor.

Semáforos (machucam meu cérebro).

  • Use a classe Semáforo para controlar o acesso a um pool de recursos. Os threads inserem o semáforo chamando o método WaitOne, que é herdado da classe WaitHandle, e libere o semáforo chamando o método Release. A contagem em um semáforo é decrementada cada vez que um segmento entra no semáforo e é incrementada quando um segmento libera o semáforo. Quando a contagem é zero, as solicitações subsequentes são bloqueadas até que outros threads liberem o semáforo. Quando todos os encadeamentos liberaram o semáforo, a contagem está no valor máximo especificado quando o semáforo foi criado. Um thread pode inserir o semáforo várias vezes .. A classe Semaphore não impõe a identidade do thread no WaitOne ou no Release .. a responsabilidade dos programadores de não estragar. Os semáforos são de dois tipos: semáforos locais e nomeadossemáforos do sistema. Se você criar um objeto Semáforo usando um construtor que aceite um nome, ele será associado a um semáforo do sistema operacional com esse nome. Os semáforos nomeados do sistema são visíveis em todo o sistema operacional e podem ser usados ​​para sincronizar as atividades dos processos. Um semáforo local existe apenas dentro do seu processo. Pode ser usado por qualquer encadeamento em seu processo que tenha uma referência ao objeto Semáforo local. Cada objeto Semáforo é um semáforo local separado.

A PÁGINA PARA LER - Sincronização de Threads (C #)

Gishu
fonte
18
Você alega que Monitornão permite a comunicação incorreta; você ainda pode Pulseetc com umMonitor
Marc Gravell
3
Confira uma descrição alternativa dos semáforos - stackoverflow.com/a/40473/968003 . Pense nos semáforos como seguranças de uma boate. Há um número dedicado de pessoas que são permitidas no clube de uma só vez. Se o clube estiver cheio, ninguém poderá entrar, mas assim que uma pessoa sair, outra pessoa poderá entrar.
Alex Klaus
31

Re "Usando outras classes de sincronização .Net" - algumas das outras que você deve conhecer:

Também há mais construções de bloqueio (baixa sobrecarga) no CCR / TPL (o Parallel Extensions CTP) - mas no IIRC, elas serão disponibilizadas no .NET 4.0

Marc Gravell
fonte
Então, se eu quiser uma comunicação de sinal simples (por exemplo, conclusão de uma operação assíncrona) - eu deveria Monitor.Pulse? ou use SemaphoreSlim ou TaskCompletionSource?
Vivek
Use TaskCompletionSource para operação assíncrona. Basicamente, pare de pensar em threads e comece a pensar em tarefas (unidades de trabalho). Threads são um detalhe de implementação e não são relevantes. Ao retornar um TCS, você pode retornar resultados, erros ou manipular o cancelamento e é facilmente compilável com outras operações assíncronas (como async waitit ou ContinueWith).
Simon Gillbee
14

Conforme declarado na ECMA, e como você pode observar nos métodos Refletidos, a instrução lock é basicamente equivalente a

object obj = x;
System.Threading.Monitor.Enter(obj);
try {
   
}
finally {
   System.Threading.Monitor.Exit(obj);
}

A partir do exemplo mencionado, vemos que os monitores podem bloquear objetos.

Os mutexe são úteis quando você precisa de sincronização entre processos, pois podem bloquear um identificador de string. O mesmo identificador de cadeia pode ser usado por diferentes processos para adquirir o bloqueio.

Os semáforos são como mutexes em esteróides, eles permitem acesso simultâneo, fornecendo uma contagem máxima de acesso simultâneo '. Quando o limite é atingido, o semáforo começa a bloquear qualquer acesso adicional ao recurso até que um dos chamadores libere o semáforo.

arul
fonte
5
Esse açúcar sintático foi ligeiramente alterado no C # 4 Confira blogs.msdn.com/ericlippert/archive/2009/03/06/…
Peter Gfader
14

Fiz as aulas e o suporte ao CLR para segmentação no DotGNU e tenho alguns pensamentos ...

A menos que você precise de bloqueios entre processos, você sempre deve evitar o uso de Mutex e Semáforos. Essas classes no .NET são invólucros em torno do Win32 Mutex e Semáforos e são bastante pesados ​​(exigem uma troca de contexto no Kernel que é cara - especialmente se o seu bloqueio não estiver em disputa).

Como outros mencionados, a instrução de bloqueio C # é mágica do compilador para Monitor.Enter e Monitor.Exit (existente em uma tentativa / finalmente).

Os monitores têm um mecanismo de sinal / espera simples, mas poderoso, que os Mutexes não possuem através dos métodos Monitor.Pulse / Monitor.Wait. O equivalente ao Win32 seria objetos de evento via CreateEvent que também existem no .NET como WaitHandles. O modelo Pulse / Wait é semelhante ao pthread_signal e ao pthread_wait do Unix, mas é mais rápido porque pode ser uma operação inteiramente no modo usuário no caso não contido.

Monitor.Pulse / Wait é simples de usar. Em um thread, bloqueamos um objeto, verificamos um sinalizador / estado / propriedade e, se não é o que esperamos, chamamos Monitor.Wait, que liberará o bloqueio e esperará até que um pulso seja enviado. Quando a espera retorna, retornamos e verificamos a flag / state / property novamente. No outro segmento, bloqueamos o objeto sempre que alteramos o sinalizador / estado / propriedade e, em seguida, chamamos PulseAll para ativar os segmentos de escuta.

Muitas vezes, queremos que nossas classes sejam seguras para threads, por isso colocamos bloqueios em nosso código. No entanto, geralmente é o caso de nossa classe ser usada apenas por um thread. Isso significa que os bloqueios abrandam desnecessariamente nosso código ... é aqui que otimizações inteligentes no CLR podem ajudar a melhorar o desempenho.

Não tenho certeza sobre a implementação de bloqueios da Microsoft, mas no DotGNU e Mono, um sinalizador de estado de bloqueio é armazenado no cabeçalho de cada objeto. Todo objeto no .NET (e Java) pode se tornar um bloqueio, portanto, todo objeto precisa oferecer suporte a isso em seus cabeçalhos. Na implementação do DotGNU, há um sinalizador que permite que você use uma hashtable global para cada objeto usado como um bloqueio - isso tem o benefício de eliminar uma sobrecarga de 4 bytes para cada objeto. Isso não é bom para a memória (especialmente para sistemas embarcados que não são muito encadeados), mas afeta o desempenho.

O Mono e o DotGNU usam efetivamente mutexes para executar o bloqueio / espera, mas usam operações de comparação e troca no estilo spinlock para eliminar a necessidade de realmente executar bloqueios, a menos que seja realmente necessário:

Você pode ver um exemplo de como os monitores podem ser implementados aqui:

http://cvs.savannah.gnu.org/viewvc/dotgnu-pnet/pnet/engine/lib_monitor.c?revision=1.7&view=markup

tumtumtum
fonte
9

Uma ressalva adicional para bloquear qualquer Mutex compartilhado que você identificou com um ID de string é que ele será padronizado como um mutex "Local \" e não será compartilhado entre as sessões em um ambiente de servidor de terminal.

Prefixe seu identificador de seqüência de caracteres com "Global \" para garantir que o acesso aos recursos compartilhados do sistema seja controlado adequadamente. Eu estava com um monte de problemas ao sincronizar as comunicações com um serviço executado na conta SYSTEM antes de perceber isso.

nvuono
fonte
5

Eu tentaria evitar "lock ()", "Mutex" e "Monitor" se você puder ...

Confira o novo System.Collections.Concurrent namespace no .NET 4
Tem algumas classes de coleção thread-safe agradáveis

http://msdn.microsoft.com/en-us/library/system.collections.concurrent.aspx

ConcurrentDictionary rocks! não há mais bloqueio manual para mim!

Peter Gfader
fonte
2
Evite bloquear, mas use o Monitor? Por quê?
Mafu
@mafutrct Porque você mesmo precisa cuidar da sincronização.
Peter Gfader
Ah, agora entendi, você pretendia evitar TODAS as três idéias mencionadas. Parecia que você usaria o Monitor, mas não o bloqueio / Mutex.
Mafu
Nunca use System.Collections.Concurrent. Eles são a principal fonte de condições de corrida e também bloqueiam o segmento de chamadores.
Alexander Danilov
-2

Na maioria dos casos, você não deve usar bloqueios (= monitores) ou mutexes / semáforos. Todos eles bloqueiam o encadeamento atual.

E você definitivamente não deve usar System.Collections.Concurrent classes - elas são a principal fonte de condições de corrida, porque não suportam transações entre várias coleções e também bloqueiam o encadeamento atual.

Surpreendentemente, o .NET não possui mecanismos eficazes de sincronização.

Eu implementei fila serial do GCD ( Objc/Swiftmundo) em C # - muito leve, sem bloquear a ferramenta de sincronização que usa o pool de threads, com testes.

É a melhor maneira de sincronizar qualquer coisa na maioria dos casos - do acesso ao banco de dados (hello sqlite) à lógica de negócios.

Alexander Danilov
fonte