ATUALIZAR
A partir do C # 6, a resposta para esta pergunta é:
SomeEvent?.Invoke(this, e);
Frequentemente, ouço / leio os seguintes conselhos:
Sempre faça uma cópia de um evento antes de verificar null
e acioná-lo. Isso eliminará um possível problema com a segmentação onde o evento se torna null
no local entre o local em que você verifica nulo e o local em que o evento é disparado:
// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;
if (copy != null)
copy(this, EventArgs.Empty); // Call any handlers on the copied list
Atualizado : pensei em ler sobre otimizações que isso também pode exigir que o membro do evento seja volátil, mas Jon Skeet afirma em sua resposta que o CLR não otimiza a cópia.
Entretanto, para que esse problema ocorra, outro segmento deve ter feito algo assim:
// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...
A sequência real pode ser essa mistura:
// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;
// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...
if (copy != null)
copy(this, EventArgs.Empty); // Call any handlers on the copied list
O ponto é que OnTheEvent
é executado após o cancelamento da assinatura do autor e, no entanto, eles simplesmente cancelam a assinatura especificamente para evitar que isso aconteça. Certamente o que é realmente necessário é uma implementação de evento personalizada com sincronização apropriada nos acessadores add
e remove
. Além disso, há o problema de possíveis conflitos se um bloqueio for mantido enquanto um evento é disparado.
Então, isso é programação de culto à carga ? Parece que sim - muitas pessoas devem seguir esta etapa para proteger seu código de vários threads, quando na realidade me parece que os eventos exigem muito mais cuidado do que isso antes que possam ser usados como parte de um design com vários threads . Consequentemente, as pessoas que não estão tomando esse cuidado adicional também podem ignorar esse conselho - simplesmente não é um problema para programas de thread único e, de fato, dada a ausência do volatile
código de exemplo on-line, o conselho pode não ter efeito em tudo.
(E não é muito mais simples atribuir o vazio delegate { }
na declaração do membro para que você nunca precise procurar null
em primeiro lugar?)
Atualizada:Caso não estivesse claro, compreendi a intenção do conselho - evitar uma exceção de referência nula em todas as circunstâncias. O que quero dizer é que essa exceção de referência nula em particular só pode ocorrer se outro segmento for excluído do evento, e a única razão para isso é garantir que nenhuma outra chamada seja recebida por esse evento, o que claramente NÃO é alcançado por essa técnica. . Você estaria escondendo uma condição de corrida - seria melhor revelá-la! Essa exceção nula ajuda a detectar um abuso do seu componente. Se você deseja que seu componente seja protegido contra abuso, siga o exemplo do WPF - armazene o ID do encadeamento em seu construtor e, em seguida, lance uma exceção se outro encadeamento tentar interagir diretamente com seu componente. Ou então, implemente um componente verdadeiramente seguro para threads (não é uma tarefa fácil).
Por isso, afirmo que apenas fazer esse idioma de cópia / verificação é uma programação de cultos de carga, adicionando bagunça e ruído ao seu código. Para realmente proteger contra outros threads, é necessário muito mais trabalho.
Atualização em resposta às postagens do blog de Eric Lippert:
Portanto, havia uma coisa importante que eu tinha perdido nos manipuladores de eventos: "é necessário que os manipuladores de eventos sejam robustos mesmo após serem cancelados", e obviamente, portanto, precisamos apenas nos preocupar com a possibilidade do evento delegar ser null
. Esse requisito nos manipuladores de eventos está documentado em algum lugar?
E assim: "Existem outras maneiras de resolver esse problema; por exemplo, inicializar o manipulador para ter uma ação vazia que nunca é removida. Mas fazer uma verificação nula é o padrão padrão".
Portanto, o único fragmento restante da minha pergunta é: por que o explícito-nulo-verifica o "padrão padrão"? A alternativa, atribuir o delegado vazio, exige apenas = delegate {}
que seja adicionada à declaração do evento, e isso elimina aquelas pequenas pilhas de cerimônia fedida de todos os lugares onde o evento é gerado. Seria fácil garantir que o delegado vazio seja barato para instanciar. Ou ainda estou faltando alguma coisa?
Certamente deve ser que (como Jon Skeet sugeriu) este é apenas o conselho do .NET 1.x que não desapareceu, como deveria ter ocorrido em 2005?
fonte
EventName(arguments)
invocar o delegado do evento incondicionalmente, em vez de apenas invocar o delegado se não for nulo (não faça nada se for nulo).Respostas:
O JIT não tem permissão para executar a otimização de que você está falando na primeira parte, devido à condição. Eu sei que isso foi criado como um espectro há um tempo atrás, mas não é válido. (Eu verifiquei com Joe Duffy ou Vance Morrison há um tempo; não me lembro qual.)
Sem o modificador volátil, é possível que a cópia local obtida esteja desatualizada, mas é tudo. Não causará a
NullReferenceException
.E sim, certamente há uma condição de corrida - mas sempre haverá. Suponha que apenas alteremos o código para:
Agora, suponha que a lista de chamadas para esse representante tenha 1000 entradas. É perfeitamente possível que a ação no início da lista tenha sido executada antes que outro encadeamento cancele a inscrição de um manipulador próximo ao final da lista. No entanto, esse manipulador ainda será executado porque será uma nova lista. (Os delegados são imutáveis.) Tanto quanto posso ver, isso é inevitável.
Usar um delegado vazio certamente evita a verificação de nulidade, mas não corrige a condição de corrida. Também não garante que você sempre "veja" o valor mais recente da variável.
fonte
Eu vejo muitas pessoas indo para o método de extensão de fazer isso ...
Isso fornece uma sintaxe melhor para aumentar o evento ...
E também elimina a cópia local, pois é capturada no momento da chamada do método.
fonte
"Por que a verificação nula explícita é o 'padrão padrão'?"
Suspeito que o motivo disso seja o fato de a verificação nula ter mais desempenho.
Se você sempre assina um representante vazio para seus eventos quando eles são criados, haverá algumas despesas gerais:
(Observe que os controles da interface do usuário geralmente têm um grande número de eventos, a maioria dos quais nunca são registrados. Ter que criar um assinante fictício para cada evento e depois invocá-lo provavelmente seria um impacto significativo no desempenho.)
Fiz alguns testes de desempenho superficial para ver o impacto da abordagem de inscrição-vazio-delegado e aqui estão meus resultados:
Observe que, no caso de zero ou um assinante (comum para controles da interface do usuário, onde os eventos são abundantes), o evento pré-inicializado com um representante vazio é notavelmente mais lento (mais de 50 milhões de iterações ...)
Para obter mais informações e código-fonte, visite esta postagem no blog sobre segurança do thread de chamada de eventos .NET que publiquei apenas um dia antes da pergunta (!)
(Minha configuração de teste pode ter falhas, fique à vontade para fazer o download do código-fonte e inspecioná-lo. Qualquer comentário é muito apreciado.)
fonte
Delegate.Combine
/Delegate.Remove
par nos casos em que o evento tem zero, um ou dois inscritos; se alguém adicionar e remover repetidamente a mesma instância delegada, as diferenças de custo entre os casos serão especialmente pronunciadas, poisCombine
apresentam um comportamento rápido de caso especial quando um dos argumentos énull
(basta retornar o outro) eRemove
é muito rápido quando os dois argumentos são igual (basta retornar nulo).Eu realmente gostei desta leitura - não! Mesmo que eu precise trabalhar com o recurso C # chamado events!
Por que não consertar isso no compilador? Eu sei que existem pessoas com esclerose múltipla que lêem essas postagens, então não chame isso!
1 - a questão Nula ) Por que não tornar os eventos vazios em vez de nulos? Quantas linhas de código seriam salvas para verificação nula ou tendo que colar um
= delegate {}
na declaração? Deixe o compilador lidar com o caso vazio, ou seja, não faça nada! Se tudo isso importa para o criador do evento, eles podem verificar .Empty e fazer o que quiserem! Caso contrário, todas as verificações / delegações nulas adicionadas são hacks em torno do problema!Honestamente, estou cansado de ter que fazer isso com todos os eventos - também conhecido como código padrão!
2 - a questão das condições da corrida ) Li o post do Eric, concordo que o H (manipulador) deve lidar quando se desferencia a si próprio, mas o evento não pode ser tornado imutável / seguro para threads? IE, defina um sinalizador de bloqueio em sua criação, para que, sempre que for chamado, ele bloqueie todas as assinaturas e cancelamentos de assinatura durante a execução?
Conclusão ,
As línguas modernas não devem resolver problemas como esses para nós?
fonte
Com o C # 6 e superior, o código pode ser simplificado usando o novo
?.
operador, como em:TheEvent?.Invoke(this, EventArgs.Empty);
Aqui está a documentação do MSDN.
fonte
De acordo com Jeffrey Richter no livro CLR via C # , o método correto é:
Porque força uma cópia de referência. Para mais informações, consulte a seção Evento no livro.
fonte
Interlocked.CompareExchange
falharia se, de alguma forma, fosse passado um nuloref
, mas isso não é o mesmo que ser passado aref
para um local de armazenamento (por exemploNewMail
) que exista e que inicialmente contenha uma referência nula.Eu tenho usado esse padrão de design para garantir que os manipuladores de eventos não sejam executados depois de serem desinscritos. Até agora, está funcionando muito bem, embora eu não tenha experimentado nenhum perfil de desempenho.
Atualmente, estou trabalhando com o Mono para Android atualmente, e o Android parece não gostar quando você tenta atualizar uma Visualização depois que sua Atividade foi enviada para segundo plano.
fonte
Esta prática não é sobre impor uma certa ordem de operações. Na verdade, trata-se de evitar uma exceção de referência nula.
O raciocínio por trás das pessoas que se preocupam com a exceção de referência nula e não com a condição de raça exigiria alguma pesquisa psicológica profunda. Eu acho que tem algo a ver com o fato de que corrigir o problema de referência nula é muito mais fácil. Uma vez consertado, eles penduram um grande banner "Missão Cumprida" em seu código e descompactam seu traje de voo.
Nota: a correção da condição de corrida provavelmente envolve o uso de uma faixa de sinalização síncrona se o manipulador deve executar
fonte
Unload
evento) cancelar com segurança as assinaturas de um objeto para outros eventos. Desagradável. Melhor é simplesmente dizer que solicitações de cancelamento de inscrição de eventos farão com que os eventos sejam cancelados "eventualmente" e que os assinantes de eventos devem verificar quando são chamados se há algo útil para eles.Então, estou um pouco atrasado para a festa aqui. :)
Quanto ao uso do padrão de objeto nulo, e não nulo, para representar eventos sem assinantes, considere este cenário. Você precisa invocar um evento, mas a construção do objeto (EventArgs) não é trivial e, no caso comum, seu evento não possui assinantes. Seria benéfico para você se você pudesse otimizar seu código para verificar se tinha algum assinante antes de comprometer o esforço de processamento para construir os argumentos e invocar o evento.
Com isso em mente, uma solução é dizer "bem, zero assinante é representado por nulo". Em seguida, basta executar a verificação nula antes de executar sua operação cara. Suponho que outra maneira de fazer isso seria ter uma propriedade Count no tipo Delegate, portanto, você só executaria a operação cara se myDelegate.Count> 0. O uso de uma propriedade Count é um padrão bastante agradável que resolve o problema original. de permitir a otimização e também possui a boa propriedade de poder ser chamada sem causar uma NullReferenceException.
Porém, lembre-se de que, como os delegados são tipos de referência, eles podem ser nulos. Talvez simplesmente não houvesse uma boa maneira de esconder esse fato oculto e oferecer suporte apenas ao padrão de objeto nulo para eventos, portanto a alternativa pode estar forçando os desenvolvedores a verificar se há assinantes nulos e zero. Isso seria ainda mais feio do que a situação atual.
Nota: Isso é pura especulação. Não estou envolvido com as linguagens .NET ou CLR.
fonte
+=
e-=
. Dentro da classe, as operações permitidas também incluiriam invocação (com verificação nula interna), testenull
ou configuração comonull
. Para qualquer outra coisa, seria necessário usar o delegado cujo nome seria o nome do evento com algum prefixo ou sufixo específico.para aplicativos de encadeamento único, você está certo de que isso não é um problema.
No entanto, se você estiver criando um componente que expõe eventos, não há garantia de que um consumidor do seu componente não vá multithreading; nesse caso, você precisará se preparar para o pior.
O uso do representante vazio resolve o problema, mas também causa um impacto no desempenho em todas as chamadas para o evento e pode ter implicações no GC.
Você está certo de que o consumidor tentou cancelar a inscrição para que isso acontecesse, mas se eles passaram pela cópia temporária, considere a mensagem já em trânsito.
Se você não usa a variável temporária e o delegado vazio, e alguém cancela a inscrição, você recebe uma exceção de referência nula, que é fatal, então acho que o custo vale a pena.
fonte
Eu realmente nunca considerei isso um problema, porque geralmente só protejo contra esse tipo de maldade potencial de segmentação em métodos estáticos (etc) em meus componentes reutilizáveis e não faço eventos estáticos.
Estou fazendo errado?
fonte
Conecte todos os seus eventos na construção e deixe-os em paz. O design da classe Delegate não pode lidar com nenhum outro uso corretamente, como explicarei no parágrafo final deste post.
Primeiro, não faz sentido tentar interceptar uma notificação de evento quando seus manipuladores de eventos já devem tomar uma decisão sincronizada sobre se / como responder à notificação .
Qualquer coisa que possa ser notificada deve ser notificada. Se os manipuladores de eventos estiverem lidando adequadamente com as notificações (ou seja, eles tiverem acesso a um estado de aplicativo autorizado e responderão somente quando apropriado), será bom notificá-los a qualquer momento e confiar que eles responderão corretamente.
A única vez que um manipulador não deve ser notificado de que um evento ocorreu é se o evento de fato não ocorreu! Portanto, se você não deseja que um manipulador seja notificado, pare de gerar os eventos (ou seja, desative o controle ou o que for responsável por detectar e trazer o evento à existência em primeiro lugar).
Honestamente, acho que a classe Delegate não pode ser salva. A fusão / transição para um MulticastDelegate foi um grande erro, porque alterou efetivamente a definição (útil) de um evento de algo que acontece em um único instante no tempo, para algo que acontece em um período de tempo. Essa alteração requer um mecanismo de sincronização que possa recolhê-lo logicamente novamente em um único instante, mas o MulticastDelegate não possui esse mecanismo. A sincronização deve abranger todo o período de tempo ou instante em que o evento ocorre, para que, uma vez que um aplicativo tome a decisão sincronizada de começar a manipular um evento, ele termine de manipulá-lo completamente (transacionalmente). Com a caixa preta que é a classe híbrida MulticastDelegate / Delegate, isso é quase impossível, entãoadote o uso de um único assinante e / ou implemente seu próprio tipo de MulticastDelegate que possui um identificador de sincronização que pode ser retirado enquanto a cadeia de manipuladores estiver sendo usada / modificada . Eu recomendo isso, porque a alternativa seria implementar a sincronização / integridade transacional de forma redundante em todos os seus manipuladores, o que seria ridiculamente / desnecessariamente complexo.
fonte
Dê uma olhada aqui: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety Esta é a solução correta e sempre deve ser usada em vez de todas as outras soluções alternativas.
“Você pode garantir que a lista de chamadas internas sempre tenha pelo menos um membro, inicializando-a com um método anônimo do-nothing. Como nenhuma parte externa pode ter uma referência ao método anônimo, nenhuma parte externa pode remover o método, para que o delegado nunca seja nulo ”- Programming .NET Components, 2nd Edition, por Juval Löwy
fonte
Não acredito que a pergunta esteja restrita ao tipo de evento c #. Removendo essa restrição, por que não reinventar um pouco a roda e fazer algo nesse sentido?
Aumentar o encadeamento de eventos com segurança - práticas recomendadas
fonte
Obrigado por uma discussão útil. Eu estava trabalhando nesse problema recentemente e fiz a seguinte classe, que é um pouco mais lenta, mas permite evitar chamadas para objetos descartados.
O ponto principal aqui é que a lista de chamadas pode ser modificada, mesmo que o evento seja gerado.
E o uso é:
Teste
Eu testei da seguinte maneira. Eu tenho um thread que cria e destrói objetos como este:
Em um
Bar
construtor (um objeto de ouvinte), assinoSomeEvent
(que é implementado como mostrado acima) e desinscrevo emDispose
:Também tenho alguns threads que aumentam o evento em um loop.
Todas essas ações são executadas simultaneamente: muitos ouvintes são criados e destruídos e o evento está sendo disparado ao mesmo tempo.
Se houvesse condições de corrida, eu deveria ver uma mensagem no console, mas ela está vazia. Mas se eu usar eventos clr como de costume, eu o vejo cheio de mensagens de aviso. Portanto, posso concluir que é possível implementar eventos seguros de thread em c #.
O que você acha?
fonte
disposed = true
acontecer antesfoo.SomeEvent -= Handler
na sua aplicação de teste, produzindo um falso positivo. Além disso, há algumas coisas que você pode querer mudar. Você realmente deseja usartry ... finally
os bloqueios - isso ajudará a tornar isso não apenas seguro para threads, mas também seguro para abortar. Sem mencionar que você poderia se livrar desse tolotry catch
. E você não está verificando se o delegado passou emAdd
/Remove
- poderia sernull
(você deve jogar imediatamente emAdd
/Remove
).