List.Add () thread safety


87

Eu entendo que, em geral, uma Lista não é segura para threads, no entanto, há algo errado em simplesmente adicionar itens a uma lista se as threads nunca executam nenhuma outra operação na lista (como percorrê-la)?

Exemplo:

List<object> list = new List<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});

3
Duplicata exata de List <T> thread safety
JK.

Certa vez, usei um List <T> apenas para adicionar novos objetos de várias tarefas executadas em paralelo. Às vezes, muito raro, ao iterar pela lista depois que todas as tarefas foram concluídas, ele acabou com um registro que era nulo, que, se nenhum encadeamento extra estivesse envolvido, seria praticamente impossível que isso acontecesse. Eu acho que, quando a lista estava realocando internamente seus elementos para expandir, de alguma forma outro thread a bagunçou ao tentar adicionar outro objeto. Portanto, não é uma boa ideia fazer isso!
osmiumbin

Exatamente o que estou vendo atualmente @osmiumbin em relação a um objeto inexplicavelmente nulo ao simplesmente adicionar de vários threads. Obrigado pela confirmação.
Blackey,

Respostas:


75

Nos bastidores, muitas coisas acontecem, incluindo realocar buffers e copiar elementos. Esse código causará perigo. Muito simplesmente, não há operações atômicas ao adicionar a uma lista, pelo menos a propriedade "Comprimento" precisa ser atualizada e o item precisa ser colocado no local correto e (se houver uma variável separada) o índice precisa ser atualizado. Vários threads podem atropelar uns aos outros. E se um cultivo for necessário, então há muito mais acontecendo. Se algo está sendo escrito para uma lista, nada mais deve ser lido ou escrito.

No .NET 4.0, temos coleções simultâneas, que são facilmente threadsafe e não requerem bloqueios.


Isso faz todo o sentido, com certeza vou olhar para as novas coleções simultâneas para isso. Obrigado.
e36M3

11
Observe que não há ConcurrentListtipo embutido . Existem bolsas, dicionários, pilhas, filas simultâneas, etc., mas nenhuma lista.
LukeH

11

Sua abordagem atual não é thread-safe - eu sugeriria evitar isso completamente - já que você basicamente faz uma transformação de dados PLINQ pode ser uma abordagem melhor (eu sei que este é um exemplo simplificado, mas no final você está projetando cada transação em outro "estado "objeto).

List<object> list = transactions.AsParallel()
                                .Select( tran => new object())
                                .ToList();

Apresentei um exemplo excessivamente simplificado para enfatizar o aspecto de List.Add no qual eu estava interessado. My Parallel.Foreach de fato fará uma boa quantidade de trabalho e não será uma simples transformação de dados. Obrigado.
e36M3

4
coleções concorrentes podem prejudicar seu desempenho paralelo se usadas desnecessariamente - outra coisa que você pode fazer é usar um array de tamanho fixo e usar a Parallel.Foreachsobrecarga que leva no índice - nesse caso, cada thread está manipulando uma entrada de array diferente e você deve estar seguro.
BrokenGlass,

6

Não é uma pergunta irracional. Não são casos em que os métodos que podem causar problemas thread-segurança em combinação com outros métodos são seguros se eles são o único método chamado.

No entanto, isso claramente não é o caso, quando você considera o código mostrado no refletor:

public void Add(T item)
{
    if (this._size == this._items.Length)
    {
        this.EnsureCapacity(this._size + 1);
    }
    this._items[this._size++] = item;
    this._version++;
}

Mesmo que EnsureCapacityfosse threadsafe em si (e certamente não é), o código acima claramente não será threadsafe, considerando a possibilidade de chamadas simultâneas para o operador de incremento causando erros de gravação.

Bloqueie, use ConcurrentList, ou talvez use uma fila sem bloqueio como o local onde vários threads gravam e leem a partir dele - seja diretamente ou preenchendo uma lista com ele - depois de terem feito seu trabalho (estou assumindo que várias gravações simultâneas seguidas por leitura de thread único é o seu padrão aqui, a julgar pela sua pergunta, caso contrário, não consigo ver como a condição em que Addo único método chamado pode ser útil).


6

Se você deseja usar a List.addpartir de vários threads e não se preocupa com a ordem, provavelmente não precisa da capacidade de indexação de a de Listqualquer maneira e deve usar algumas das coleções simultâneas disponíveis.

Se você ignorar este conselho e apenas fizer isso add, poderá tornar o addtópico seguro, mas em uma ordem imprevisível como esta:

private Object someListLock = new Object(); // only once

...

lock (someListLock)
{
    someList.Add(item);
}

Se você aceitar essa ordem imprevisível, é provável que, conforme mencionado anteriormente, não precise de uma coleção que possa fazer a indexação como em someList[i].


5

Isso causaria problemas, pois a lista é construída sobre uma matriz e não é segura para threads, você pode obter a exceção de índice fora dos limites ou alguns valores substituindo outros valores, dependendo de onde as threads estão. Basicamente, não faça isso.

Existem vários problemas potenciais ... Apenas não. Se você precisar de uma coleção de thread safe, use um bloqueio ou uma das coleções System.Collections.Concurrent.



2

Há algo de errado em simplesmente adicionar itens a uma lista se os threads nunca executam nenhuma outra operação na lista?

Resposta curta: sim.

Resposta longa: execute o programa abaixo.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

class Program
{
    readonly List<int> l = new List<int>();
    const int amount = 1000;
    int toFinish = amount;
    readonly AutoResetEvent are = new AutoResetEvent(false);

    static void Main()
    {
        new Program().Run();
    }

    void Run()
    {
        for (int i = 0; i < amount; i++)
            new Thread(AddTol).Start(i);

        are.WaitOne();

        if (l.Count != amount ||
            l.Distinct().Count() != amount ||
            l.Min() < 0 ||
            l.Max() >= amount)
            throw new Exception("omg corrupted data");

        Console.WriteLine("All good");
        Console.ReadKey();
    }

    void AddTol(object o)
    {
        // uncomment to fix
        // lock (l) 
        l.Add((int)o);

        int i = Interlocked.Decrement(ref toFinish);

        if (i == 0)
            are.Set();
    }
}

@royi, você está executando isso em uma máquina de núcleo único?
Bas Smit

Oi, eu acho que há um problema com este exemplo, pois ele está definindo o AutoResetEvent sempre que encontrar o número 1000. Porque ele pode processar esses segmentos sempre que quiser, pode chegar a 1000 antes de chegar a 999 por exemplo. Se você adicionar um Console.WriteLine no método AddTol, verá que a numeração não está em ordem.
Dave Walker,

@dave, está definindo o evento quando i == 0
Bas Smit

2

Como outros já disseram, você pode usar coleções simultâneas do System.Collections.Concurrentnamespace. Se você pode usar um desses, é preferível.

Mas se você realmente quiser uma lista que está apenas sincronizada, você pode olhar para SynchronizedCollection<T>-Class em System.Collections.Generic.

Observe que você teve que incluir o assembly System.ServiceModel, que também é o motivo pelo qual não gosto tanto dele. Mas às vezes eu uso.


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.