A iteração sobre uma matriz com um loop for é uma operação segura de thread em C #? Que tal iterar um IEnumerable <T> com um loop foreach?

8

Com base no meu entendimento, dado um array C #, o ato de iterar sobre o array simultaneamente de vários threads é uma operação segura para threads.

Ao iterar sobre a matriz, quero dizer a leitura de todas as posições dentro da matriz por meio de um loop antigo simplesfor . Cada thread está simplesmente lendo o conteúdo de um local de memória dentro da matriz, ninguém está escrevendo nada, então todos os threads lêem a mesma coisa de maneira consistente.

Este é um pedaço de código fazendo o que escrevi acima:

public class UselessService 
{
   private static readonly string[] Names = new [] { "bob", "alice" };

   public List<int> DoSomethingUseless()
   {
      var temp = new List<int>();

      for (int i = 0; i < Names.Length; i++) 
      {
        temp.Add(Names[i].Length * 2);
      }

      return temp;
   }
}

Portanto, meu entendimento é de que o método DoSomethingUseless é seguro para threads e que não há necessidade de substituí-lo string[]por um tipo seguro de thread (como ImmutableArray<string>por exemplo).

Estou correcto ?

Agora vamos supor que temos uma instância de IEnumerable<T>. Não sabemos qual é o objeto subjacente, apenas sabemos que temos um objeto implementado IEnumerable<T>, portanto, podemos iterá-lo usando o foreachloop.

Com base no meu entendimento, nesse cenário, não garantia de que a iteração desse objeto a partir de vários threads simultaneamente seja uma operação segura de thread. Em outras palavras, é perfeitamente possível que a iteração sobre a IEnumerable<T>instância de diferentes threads ao mesmo tempo quebre o estado interno do objeto, para que ele seja corrompido.

Estou correto neste ponto?

E a IEnumerable<T>implementação da Arrayclasse? É thread seguro?

Em outras palavras, o seguinte encadeamento de código é seguro? (esse é exatamente o mesmo código acima, mas agora a matriz é iterada usando um foreachloop em vez de um forloop)

public class UselessService 
{
   private static readonly string[] Names = new [] { "bob", "alice" };

   public List<int> DoSomethingUseless()
   {
      var temp = new List<int>();

      foreach (var name in Names) 
      {
        temp.Add(name.Length * 2);
      }

      return temp;
   }
}

Existe alguma referência informando que IEnumerable<T>implementações na biblioteca de classes base do .NET são realmente seguras contra threads?

Enrico Massone
fonte
4
@Christopher: Re: seu primeiro comentário: no cenário de Enrico, onde uma matriz está sendo lida apenas e nem o armazenamento da referência da matriz nem os elementos da matriz estão sendo alterados, um cenário de múltiplos leitores deve ser seguro sem barreiras adicionais.
Eric Lippert
3
@Christopher "Não, a iteração sobre uma matriz não é salva no thread." - A iteração sobre uma matriz (ou seja Array) será segura para threads, desde que todas as threads estejam apenas lendo. O mesmo com List<T>e provavelmente todas as outras coleções escritas pela Microsoft. Mas é possível criar o seu próprio IEnumerableque faz coisas seguras sem thread no enumerador. Portanto, não é uma garantia de que tudo o que implementa IEnumerableseja seguro para threads.
Gabriel Luci
6
@ Christopher: Re: seu segundo comentário: não, foreachfunciona com mais do que apenas enumeradores. O compilador é inteligente o suficiente para saber que, se tiver informações de tipo estático indicando que a coleção é uma matriz, ele pula a geração do enumerador e acessa a matriz diretamente como se fosse um forloop. Da mesma forma, se a coleção suportar uma implementação personalizada de GetEnumerator, essa implementação será usada em vez de chamar IEnumerable.GetEnumerator. A noção em que foreachapenas trafega IEnumerable/IEnumeratoré falsa.
Eric Lippert
3
@ Christopher: Re: seu terceiro comentário: tenha cuidado. Embora possa haver algo como "obsoleto", a noção de que também exista algo como "novo" não é precisa. volatilenão garante que você obtenha a atualização mais recente possível para uma variável porque o C # permite que diferentes segmentos observem diferentes ordenações de leituras e gravações e, portanto, a noção de "mais recente" não é definida globalmente . Em vez de raciocinar sobre atualização, raciocine sobre quais restrições volatile colocam a forma como as gravações podem ser reordenadas.
Eric Lippert
6
@EnricoMassone: Não estou tentando responder a nenhuma de suas perguntas, mas noto que suas perguntas indicam que você deseja fazer algo muito arriscado, a saber, escrever uma solução de baixo ou nenhum bloqueio que compartilhe memória através de threads. Eu não tentaria fazê-lo, exceto nos casos em que realmente não havia outra opção que atendesse às minhas metas de desempenho e nos casos em que me sentisse confiante em analisar minuciosamente e com precisão todas as reordenações possíveis de todas as leituras e gravações no programa . Como você está fazendo essas perguntas, acho que você se sente menos confiante do que eu ficaria à vontade.
precisa

Respostas:

2

A iteração sobre uma matriz com um loop for é uma operação segura de thread em C #?

Se você está falando estritamente sobre a leitura de vários segmentos , que será segmento seguro para Arraye List<T>e apenas sobre cada coleção escrito por Microsoft, independentemente de se você estiver usando um forou foreachloop. Especialmente no exemplo, você tem:

var temp = new List<int>();

foreach (var name in Names)
{
  temp.Add(name.Length * 2);
}

Você pode fazer isso em quantos threads quiser. Todos lerão os mesmos valores Namesfelizes.

Se você escrever a partir de outro tópico (essa não foi sua pergunta, mas vale a pena notar)

Iterando sobre um loop Arrayou List<T>com um forloop, ele continuará lendo e lerá alegremente os valores alterados à medida que você os encontrar.

Iterando com um foreachloop, isso depende da implementação. Se um valor de uma forma Arraymudar parcialmente através de um foreachloop, ele continuará enumerando e fornecendo os valores alterados.

Com List<T>, depende do que você considera "thread safe". Se você está mais preocupado com a leitura de dados precisos, é meio "seguro", pois gera uma exceção no meio da enumeração e informa que a coleção foi alterada. Mas se você considera que lançar uma exceção não é segura, não é seguro.

Mas vale a pena notar que esta é uma decisão de design List<T>, existe um código que procura explicitamente alterações e gera uma exceção. As decisões de design nos levam ao próximo ponto:

Podemos assumir que toda coleção implementada IEnumerableé segura para ler em vários segmentos?

Na maioria dos casos , será, mas a leitura segura de threads não é garantida. O motivo é que todos IEnumerableexigem uma implementação de IEnumerator, que decide como percorrer os itens da coleção. E, como em qualquer classe, você pode fazer o que quiser , incluindo coisas não seguras para threads, como:

  • Usando variáveis ​​estáticas
  • Usando um cache compartilhado para ler valores
  • Não fazendo nenhum esforço para lidar com casos em que a coleção é alterada na enumeração intermediária
  • etc.

Você pode até fazer algo estranho, como fazer GetEnumerator()retornar a mesma instância do seu enumerador toda vez que for chamado. Isso poderia realmente gerar alguns resultados imprevisíveis.

Considero que algo não é seguro para threads, se puder resultar em resultados imprevisíveis. Qualquer uma dessas coisas pode causar resultados imprevisíveis.

Você pode ver o código-fonte do Enumeratorque List<T>usa , para que ele não faça nada estranho, o que indica que a enumeração List<T>de vários segmentos é segura.

Gabriel Luci
fonte
portanto, basicamente, se alguém fornecer uma instância de IEnumerable e a única coisa que você souber sobre ela for implementar o IEnumerable, não será seguro enumerar esse objeto de diferentes threads. Caso contrário, se você tiver uma instância de uma classe BCL, digamos uma instância de List <string>, poderá enumerar com segurança o objeto a partir de vários encadeamentos (mesmo que o próprio tipo de List <string> não seja seguro para todos os seus membros, você pode ter certeza de que sua implementação da interface IEnumerable é segura para threads).
Enrico Massone
1
toda a discussão sobre a segurança de enumerações simultâneas de coleções BCL é baseada em uma suposição: podemos ler o código-fonte fornecido pela microsoft e, atualmente, não conhecemos nenhum exemplo de classe BCL que não seja segura em relação a concorrentes enumeração. Basicamente, estamos contando com um detalhe de implementação. A menos que algo não esteja explicitamente documentado, fazer suposições não é a opção mais segura. Portanto, a opção mais segura é provavelmente evitar problemas e usar algo como um ImmutableArray <string>, que sabemos com certeza sendo thread thread safe devido à sua imutabilidade.
Enrico Massone
1
"a opção mais segura é provavelmente evitar problemas e usar algo como um ImmutableArray <string>" - se você tem controle sobre o tipo de coleção e sabe que estará lendo apenas os threads, List<T>ou Arrayé tão seguro quanto ImmutableArray<string>, pois podemos provar que esses são tópicos seguros para ler. Eu só procuraria em outras opções se você planeja ler e escrever em vários threads. Você pode usar uma coleção apropriada de System.Collections.Concurrent.
Gabriel Luci
1

Afirmar que seu código é seguro para threads significa que devemos tomar suas palavras como garantidas de que não há código dentro do UselessServiceque tentará substituir simultaneamente o conteúdo da Namesmatriz por algo como "tom" and "jerry"ou (mais sinistro) null and null. Por outro lado, utilizando um ImmutableArray<string>iria garantir que o código é thread-safe, e todo mundo poderia ter certeza sobre isso só de olhar o tipo do campo somente leitura estático, sem ter de inspeccionar cuidadosamente o resto do código.

Você pode achar interessantes esses comentários no código-fonte do ImmutableArray<T>, sobre alguns detalhes de implementação dessa estrutura:

Uma matriz somente leitura com tempo de pesquisa indexável O (1).

Esse tipo tem um contrato documentado de ter exatamente um campo do tipo de referência em tamanho. Nossa própria System.Collections.Immutable.ImmutableInterlockedclasse depende disso, assim como outras externamente.

AVISO IMPORTANTE PARA MANUTORES E REVISORES:

Esse tipo deve ser seguro para threads. Como uma estrutura, ele não pode proteger seus próprios campos de serem alterados de um encadeamento enquanto seus membros estão executando em outros encadeamentos, porque as estruturas podem mudar de lugar simplesmente reatribuindo o campo que contém essa estrutura. Portanto, é extremamente importante que Todo membro desreferencie uma thisvez. Se um membro precisar fazer referência ao campo da matriz, isso conta como uma desreferência de this. Chamar outros membros da instância (propriedades ou métodos) também conta como desreferenciação this. Qualquer membro que precise usar thismais de uma vez deve atribuirthispara uma variável local e use-a no restante do código. Isso efetivamente copia o campo one na estrutura para uma variável local, para que seja isolado de outros threads.

Theodor Zoulias
fonte