2015-07-16 3 views
1

В моем реальном приложении мне нужно итерации коллекции, но ее можно изменить из другой темы. Поэтому мне нужно скопировать коллекцию, чтобы перебирать ее. Я воспроизвел эту проблему на небольшом примере, но, по-видимому, мое отсутствие понимания замков и потоков приводит к System.ArgumentException. Пробовал разные вещи с помощью блокировки, но результат тот же.System.ArgumentException при блокировке

class Program 
{ 
    static List<int> list; 
    static void Main(string[] args) 
    { 
     list = new List<int>(); 
     for (int i = 0; i < 1000000; i++) 
     { 
      list.Add(i); 
      if (i == 1000) 
      { 
       Thread t = new Thread(new ThreadStart(WorkThreadFunction)); 
       t.Start(); 
      } 
     } 
    } 

    static void WorkThreadFunction() 
    { 
     lock (list) 
     { 
      List<int> tmp = list.ToList(); //Exception here! 
      Console.WriteLine(list.Count); 
     } 
    } 
} 
+0

Каково сообщение об этом исключении? –

ответ

0

Пробовал предложения Джоэла. ConcurrentBag был очень медленным. Блокировка на каждой из миллионов итераций кажется неэффективной. Похоже, что в этом случае хорошие записи событий ждут (занимает 3 раза меньше, чем с помощью блокировок на моем компьютере).

class Program 
{ 
    static List<int> list; 
    static ManualResetEventSlim mres = new ManualResetEventSlim(false); 

    static void Main(string[] args) 
    { 
     list = new List<int>(); 
     for (int i = 0; i < 10000000; i++) 
     { 
      list.Add(i); 

      if (i == 1000) 
      { 
       Thread t = new Thread(new ThreadStart(WorkThreadFunction)); 
       t.Start(); 
       mres.Wait(); 
      } 
     } 
    } 

    static void WorkThreadFunction() 
    { 
     List<int> tmp = list.ToList(); 
     Console.WriteLine(list.Count); 
     mres.Set(); 
    } 
} 
+2

Просто добавьте, что 'Wait()' и 'Set()' приведет к блокировке вашего основного потока во время работы 'WorkThreadFunction()'. Вы всегда увидите, что ваш 'list.Count' будет равен 1001 (пока не вызывается' Set() '), поскольку ваш поток теперь синхронизирован. Это означает, что никакая операция добавления в список не будет выполняться до завершения «WorkThreadFunction». –

1

Вариант 1:

Вот модифицированная версия кода:

class Program 
{ 
    static List<int> list; 
    static void Main(string[] args) 
    { 
     list = new List<int>(); 
     for (int i = 0; i < 1000000; i++) 
     { 
      lock (list) //Lock before modification 
      { 
       list.Add(i); 
      } 
      if (i == 1000) 
      { 
       Thread t = new Thread(new ThreadStart(WorkThreadFunction)); 
       t.Start(); 
      } 

     } 

     Console.ReadLine(); 
    } 

    static void WorkThreadFunction() 
    { 
     lock (list) 
     { 
      List<int> tmp = list.ToList(); //Exception here! 
      Console.WriteLine(list.Count); 
     } 
    } 
} 

Что здесь происходит, что ваш list модифицируется, будучи преобразованы в другой коллекции списка (где исключение аргумента). Поэтому, чтобы избежать этого, вам нужно будет заблокировать список, как показано выше.

Вариант 2: (Нет lock)

Использование Одновременные коллекции для удаления lock:

using System.Collections.Concurrent; 

//Change this line 
static List<int> list; 
//To this line 
static ConcurrentBag<int> list; 

И удалить все lock заявления.

+0

Работает, но это замедляет выполнение программы почти в 5 раз. Как мы можем заблокировать список только тогда, когда нам нужно его скопировать, но не миллион раз при каждой операции добавления? – Dork

+0

Пробовали ли вы использовать потокобезопасные коллекции с https://msdn.microsoft.com/en-us/library/system.collections.concurrent(v=vs.110).aspx, вы можете использовать 'ConcurrentBag ' и удалять «замок». Вы можете сравнить эффективность выполнения. –

+0

С ConcurrentBag требуется даже 17 раз дольше, чем без блокировок, и в 3 раза дольше, чем с помощью замков :( – Dork

1

Я вижу некоторые проблемы в вашем алгоритме, и, возможно, вам следует реорганизовать его. В случае использования класса locks или ConcurrentBag вы должны понимать, что копирование всей коллекции в новую просто для перечисления - это очень большая и очень трудоемкая операция, и во время нее вы не можете эффективно работать с коллекцией.

lock (list) 
{ 
    // VERY LONG OPERATION HERE 
    List<int> tmp = list.ToList(); //Exception here! 
    Console.WriteLine(list.Count); 
} 

Вы действительно не должны lock сбор за такое количество времени - в конце цикла for у вас есть много Threads, которые блокируют друг друга. Вы должны использовать TPL classes для этого подхода и не должны напрямую использовать Threads.

В другом случае вы можете реализовать некоторые из optimistic lock-free algorithm с двойной проверкой на версию коллекции или даже lock-free and wait-free algorithm с сохранением моментального снимка коллекции и проверкой ее внутри ваших методов доступа к коллекции. Additional information can be found here.

Я думаю, что предоставленной вами информации недостаточно, чтобы предложить вам правильный способ решить вашу проблему.

+0

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

+1

Если это так, вам определенно нужно правильно определить ваш алгоритм, возможно, с итерацией по частям коллекции. – VMAtm

+0

Perfect. Я могу взять граф коллекции в данный момент и повторить его, используя цикл. (не проблема в моем случае, если элемент будет добавлен при чтении). int n = list.Count. list.Take (n) выдает одно и то же исключение, поэтому мне нужно будет использовать для цикла вместо LINQ, но все в порядке. – Dork

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