9

Следующий код объясняет мой вопрос. Я знаю, что список не является потокобезопасным. Но какова основная «настоящая» причина этого?несколько потоков, добавляющих элементы в один список. почему в списке всегда меньше элементов, чем ожидалось?

class Program 
{ 
    static void Main(string[] args) 
    { 
     List<string> strCol = new List<string>(); 

     for (int i = 0; i < 10; i++) 
     { 
      int id = i; 
      Task.Factory.StartNew(() => 
      { 
       AddElements(strCol); 
      }).ContinueWith((t) => { WriteCount(strCol, id.ToString()); }); 
     } 

     Console.ReadLine(); 
    } 

    private static void WriteCount(List<string> strCol, string id) 
    { 
     Console.WriteLine(string.Format("Task {0} is done. Count: {1}. Thread ID: {2}", id, strCol.Count, Thread.CurrentThread.ManagedThreadId)); 
    } 

    private static void AddElements(List<string> strCol) 
    { 
     for (int i = 0; i < 20000; i++) 
     { 
      strCol.Add(i.ToString()); 
     } 
    } 
} 
+1

похоже, что некоторые потоки пишут друг над другом. Внутренний список должен использовать счетчик, чтобы увеличить его до следующей позиции, а так как он не защищен потоком, его значения перезаписи –

ответ

15

Я пропущу очевидный ответ «Список не является потокобезопасным» - это вы уже знаете.

Элементы списка хранятся во внутреннем массиве. При добавлении элемента в список есть как минимум два этапа (с логической точки зрения). Во-первых, List получает индекс, указывающий, куда положить новый элемент. Он помещает новый элемент в массив, используя этот индекс. Затем он увеличивает индекс, и это второй этап. Если во втором (или третьем, четвертом, ...) потоке происходит добавление нового элемента одновременно, может быть, будут добавлены два (3, 4, ...) новые элементы, помещенные в одно и то же расположение массива до индекса увеличивается на первый поток. Элементы перезаписываются и теряются.

Внутренние операции добавления нового элемента и индекса инкремента должны всегда выполняться за один раз, чтобы список был потокобезопасным. Это то, что называется критической секцией. Это может быть достигнуто замками.

Надеюсь, это объяснит немного.

13

Это потому, что List<T> не является потокобезопасным.

Для этого вам следует использовать поточную безопасную коллекцию, такую ​​как одна из коллекций в System.Collections.Concurrent. В противном случае вам нужно будет синхронизировать весь доступ к List<T> (т. Е. Поместить каждый вызов Add в блокировку), что приведет к победе в цели вызова этого с использованием нескольких потоков полностью, поскольку вы не выполняете какую-либо другую работу в этой ситуации ,

+0

, как указано в вопросе. Я знаю, что список не является потокобезопасным. Но какова основная «настоящая» причина этого? – CuiPengFei

+2

@CuiPengFei «настоящая» причина, вероятно, состоит в том, что есть операции (копии данных, размеры массивов и т. Д.), Которые выполняются без блокировки, в предположении, что разработчик не взаимодействует с «списком» из нескольких потоков одновременно. Дополнительная синхронизация, необходимая для обеспечения безопасности потока «List», значительно повлияла бы на производительность, поэтому дизайнеры BCL решили опустить ее и документировать отсутствие безопасности потоков, так что если вам нужен поточный безопасный доступ, вы можете сами создать его вокруг списка. То же, что и в Java ArrayList и многих других классах List. –

+0

@CuiPengFei: Потому что, не будучи потокобезопасным, акт добавления в 'List ' не является полностью атомным. Один поток начинает добавлять элемент, другой поток замыкает его, добавляя свой собственный элемент. – David

Смежные вопросы