Список с ненулевыми элементами в конечном итоге содержит ноль.Проблема с синхронизацией? - PullRequest
3 голосов
/ 26 апреля 2010

Прежде всего, извините за заголовок - я не мог найти тот, который был коротким и достаточно ясным.

Вот проблема: у меня есть список List<MyClass> list, к которому я всегда добавляю только что созданные экземпляры MyClass, например: list.Add(new MyClass()). Я не добавляю элементы другим способом.

Однако затем я перебираю список с foreach и обнаруживаю, что есть несколько пустых записей. То есть следующий код:

foreach (MyClass entry in list)
    if (entry == null)
         throw new Exception("null entry!");

иногда выдает исключение. Следует отметить, что list.Add(new MyClass()) выполняются из разных потоков, работающих одновременно. Единственное, что я могу придумать для учета записей null, - это одновременный доступ. List<> в конце концов не является поточно-ориентированным. Хотя я все еще нахожу странным, что в итоге он содержит пустые записи, а не просто не дает никаких гарантий при заказе.

Можете ли вы придумать какую-либо другую причину?

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

Спасибо.


РЕДАКТИРОВАТЬ : простое решение, основанное на предложении Кевина:

public class AsynchronousList<T> : List<T> {

    private AddDelegate addDelegate;
    public delegate void AddDelegate(T item);

    public AsynchronousList() {
        addDelegate = new AddDelegate(this.AddBlocking);
    }

    public void AddAsynchronous(T item) {
        addDelegate.BeginInvoke(item, null, null);
    }

    private void AddBlocking(T item) {
        lock (this) {
            Add(item);
        }
    }
}

Мне нужно только управлять Add операциями, и мне просто нужно это для отладки (этого не будет в конечном продукте), поэтому я просто хотел быстро исправить.

Спасибо всем за ваши ответы.

Ответы [ 3 ]

8 голосов
/ 26 апреля 2010

List<T> может поддерживать только несколько читателей одновременно. Если вы собираетесь использовать несколько потоков для добавления в список, вам сначала нужно заблокировать объект. На самом деле нет никакого способа обойти это, потому что без блокировки вы все равно можете сделать так, чтобы кто-то читал из списка, в то время как другой поток обновляет его (или несколько объектов, пытающихся обновить его одновременно).

http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx

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

4 голосов
/ 26 апреля 2010

Единственное, что я могу придумать, чтобы учесть нулевые записи, - это одновременный доступ. List<> не является поточно-ориентированным, в конце концов.

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

Относительно того, почему возникает эта конкретная проблема, мы можем только предположить, поскольку частная реализация List<>, ну, в общем, частная ( Я знаю, что у нас есть Reflector и Shared Source - но в принципе это частная ). Предположим, что реализация включает в себя массив и «последний заполненный индекс». Предположим также, что «Добавить предмет» выглядит так:

  • Убедитесь, что массив достаточно велик для другого элемента
  • последний заполненный индекс <- последний заполненный индекс + 1 </li>
  • массив [последний заполненный индекс] = входящий элемент

Теперь предположим, что есть два потока, вызывающих Add. Если последовательность операций чередования заканчивается так:

  • Тема A: последний заполненный индекс <- последний заполненный индекс + 1 </li>
  • Тема B: последний заполненный индекс <- последний заполненный индекс + 1 </li>
  • Тема A: массив [последний заполненный индекс] = входящий элемент
  • Тема B: массив [последний заполненный индекс] = входящий элемент

тогда не только будет null в массиве, но и элемент, который поток А пытался добавить, вообще не будет в массиве!

Так вот, я не точно знаю , как List<> внутренне работает. У меня есть половина памяти, что это с ArrayList, который внутренне использует эту схему; но на самом деле это не имеет значения. Я подозреваю, что любой любой механизм списков, который предполагается запустить не одновременно, может быть сломан при одновременном доступе и достаточно «неудачном» чередовании операций. Если мы хотим обеспечить безопасность потоков от API, который его не предоставляет, у нас есть , чтобы выполнить некоторую работу самостоятельно - или, по крайней мере, мы не должны удивляться, если API иногда ломает его, когда мы не т.

Для вашего требования

Я не хочу, чтобы вызывающие потоки блокировали ожидание добавления своего элемента

моей первой мыслью была очередь Multiple Producer-Single-Consumer, в которой потоки, желающие добавить элементы, являются производителями, которые отправляют элементы в очередь асинхронно, и есть один потребитель, который извлекает элементы из очереди и добавляет их в список с соответствующей блокировкой. Моя вторая мысль заключается в том, что мне кажется, что это будет тяжелее, чем того требует ситуация, поэтому я позволю себе немного обдумать.

1 голос
/ 26 апреля 2010

Если вы используете .NET Framework 4, вы можете проверить новые Concurrent Collections . Когда дело доходит до многопоточности, лучше не пытаться быть умным, потому что очень легко ошибиться. Синхронизация может повлиять на производительность, но последствия неправильной многопоточности могут также привести к странным, нечастым ошибкам, которые очень трудно отследить.

Если вы все еще используете Framework 2 или 3.5 для этого проекта, я рекомендую просто поместить ваши вызовы в список в операторе блокировки. Если вы беспокоитесь о производительности Add (выполняете ли вы какую-то длительную операцию, используя список где-то еще?), Вы всегда можете сделать копию списка в пределах блокировки и использовать эту копию для своей длительной операции вне замок. Простая блокировка самих надстроек не должна быть проблемой производительности, если у вас не очень большое количество потоков. В этом случае вы можете попробовать очередь Multiple Producer-Single-Consumer, рекомендованную AakashM.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...