2013-02-18 3 views
7

Хотя ответ на this question превосходный, это означает, что вы должны окружать вызовы List.ToArray() в блокировке для параллелизма. this blog post также подразумевает, что он может катастрофически (но редко). Обычно я использую ToArray вместо блокировки при перечислении списков или других коллекций, чтобы исключить исключение «Collection Modified, Enumeration not complete». Этот ответ и сообщение в блоге вызвали это предположение.Может ли ToArray() выдавать исключение?

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

Если это предположение неверно, то, хотя оно никогда не вызывало проблемы, это может быть временная бомба в приложении с высокой степенью готовности. Каков окончательный ответ?

+0

Список .Add также не генерирует исключение, если отдельные темы изменяют список одновременно. Он по-прежнему не является потокобезопасным. Он просто проверяет, что вы не изменяете и не перечисляете его одновременно в одном и том же потоке. Почему вы считаете, что метод, который не документирован как потокобезопасный, может быть потокобезопасным? (Предполагается, что вы говорите о списке .ToArray или Enumerable.ToArray. ConcurrentBag .ToArray потокобезопасно, как перечислив ConcurrentBag без ToArray.) – dtb

+0

я сузили сферу вопроса, чтобы сосредоточить внимание на список и список так как это те, кого меня больше всего волнует. Я видел эту технику, используемую во многих популярных фреймворках с открытым исходным кодом, поэтому я не думаю, что я единственный, кто делает предположение о «безопасности» этой техники. Я не спрашиваю, являются ли они потокобезопасными (по определению это не так), но я должен начать поиск и исправление любых случаев «небезопасных» вызовов ToArray()? –

+0

Вы уверены, что эти рамки с открытым исходным кодом используют ToArray, а не блокировку, или они фактически используют ToArray для изменения списка, когда они перечисляют его в том же потоке? – dtb

ответ

5

Вы не найдете документацию о возможных исключениях из ToArray по одной простой причине. Это метод расширения, который имеет много «перегрузок». Все они имеют одну и ту же подпись метода, но реализация различна для разных типов сбора, например. List<T> и HashSet<T>.

Однако мы можем сделать безопасное предположение для большей части кода, который .NET framework BCL не выполняет никакой блокировки по соображениям производительности. Я также специально проверил реализацию ToList для List<T>.

public T[] ToArray() 
{ 
    T[] array = new T[this._size]; 
    Array.Copy(this._items, 0, array, 0, this._size); 
    return array; 
} 

Как вы могли себе представить, что это довольно простой код, который заканчивает выполнение в mscorlib. Для этой конкретной реализации вы также можете увидеть исключения, которые могут возникнуть in MSDN page for Array.Copy method. Это сводится к исключению, которое выбрасывается, если ранг списка изменяется сразу после того, как целевой массив был просто выделен.

Имея в виду, что List<T> является тривиальным примером, вы можете себе представить, что шансы на рост исключений на структуры требуют более сложного кода для хранения в массиве. Реализация для Queue<T> является кандидатом, который, скорее всего, на провал:

public T[] ToArray() 
{ 
    T[] array = new T[this._size]; 
    if (this._size == 0) 
    { 
     return array; 
    } 
    if (this._head < this._tail) 
    { 
     Array.Copy(this._array, this._head, array, 0, this._size); 
    } 
    else 
    { 
     Array.Copy(this._array, this._head, array, 0, this._array.Length - this._head); 
     Array.Copy(this._array, 0, array, this._array.Length - this._head, this._tail); 
    } 
    return array; 
} 
+0

Отличный анализ. Это отвечает на вопрос окончательно. Благодаря! –

0

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

Во-вторых, вам нужно будет прояснить «что» функция перечисления на самом деле, так как это будет отличаться от типа перечисления, в котором вы работаете, - вы говорите о нормальном расширении linq для перечислений?

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

И так далее.

+2

Я бы поддержал вас, но последнее предложение действительно не было необходимо. Не все полностью соответствуют теории безопасности потоков и не могут быть ожидаемыми. –

+0

Я стою исправлен, как и мой текст. Я поддержал вас за то, что дал мне хороший урок;) –

4

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

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

Сделайте это правильно и заблокируйте.

Btw, эти замечания были для List.ToArray, который является одной из наиболее безопасных версий ToArray. Я понимаю, почему ошибочно полагаю, что он может использоваться одновременно с записью в список. Конечно IEnumerable.ToArrayне может быть threadsafe, потому что это свойство базовой последовательности.

+0

-1 - это потому, что ответ слишком многословен и слишком обобщен. Иногда правильная вещь - блокировка, но иногда правильная вещь - выбирать поток или параллельную структуру данных/алгоритм. Кроме того, есть * * тесты, которые вы можете выполнить на коде, который должен быть потокобезопасным, чтобы проверить его. –

+0

@ 280Z28 Я уважаю вашу критику. Ответ должен был быть достаточно общим, потому что основная проблема одинакова для целого класса вопросов. Я просто подозрительно отношусь к вашему последнему заявлению: какие тесты вы проведете, чтобы гарантировать, что ToArray на 100% безопасен? Все, что меньше, является отправкой бомбу в производство. Вы когда-нибудь делали операцию? Я * ненавижу * получаю 10 сообщений об ошибках в день из неопределенной ошибки. – usr

+0

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

0

Кажется, вы путаете две вещи:

  • < Список T > не поддерживает модифицируется в то время как перечислено. При перечислении списка перечислитель проверяет, был ли список изменен после каждой итерации. Список вызовов <T> .ToArray перед перечислением списка решает эту проблему, поскольку вы перечисляете моментальный снимок списка, а не сам список.

  • Список <T> не является потокобезопасной коллекцией. Все вышеперечисленное предполагает доступ из одного потока. Доступ к списку из двух потоков всегда требует блокировки. Список <T> .ToArray не является потокобезопасным и здесь не помогает.

2

ToArray НЕ является потоковым, и этот код доказывает это!

Рассмотрим довольно нелепый код:

 List<int> l = new List<int>(); 

     for (int i = 1; i < 100; i++) 
     { 
      l.Add(i); 
      l.Add(i * 2); 
      l.Add(i * i); 
     } 

     Thread th = new Thread(new ThreadStart(() => 
     { 
      int t=0; 
      while (true) 
      { 
       //Thread.Sleep(200); 

       switch (t) 
       { 
        case 0: 
         l.Add(t); 
         t = 1; 
         break; 
        case 1: 
         l.RemoveAt(t); 
         t = 0; 
         break; 
       } 
      } 
     })); 

     th.Start(); 

     try 
     { 
      while (true) 
      { 
       Array ai = l.ToArray(); 

       //foreach (object o in ai) 
       //{ 
       // String str = o.ToString(); 
       //} 
      } 
     } 
     catch (System.Exception ex) 
     { 
      String str = ex.ToString();     
     } 

    } 

Этот код потерпит неудачу в очень краткосрочной перспективе, из-за l.Add(t) линии. Поскольку ToArray НЕ является потокобезопасным, он будет выделять массив на текущий размер l, затем мы добавим элемент в l (в другом потоке), а затем попытаемся скопировать текущий размер l в ai и сбой потому что у меня слишком много элементов. ToArray выбрасывает ArgumentException.

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