7

У меня была необходимость многопоточной структуры данных, которая поддерживает эти требования:Существует ли существующее решение проблемы многопоточной структуры данных?

  • Позволяет несколько одновременных читателей и писателей
  • сортируется
  • ли легко рассуждать о

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

Я занимаюсь исследованиями в этой области, и я знаю о ConcurrentSkipList (по Lea на основе работы Fraser и Harris), поскольку он реализован в Java SE 6. Я также внедрил свою собственную версию параллельный Пропустить Список, основанный на A Provably Correct Scalable Concurrent Skip List Херлихи, Лев, Лучанго и Шавит.

Эти две реализации разработаны людьми, которые являются светлыми годами более умными, чем я, но я все еще (несколько стыдно, потому что это потрясающая работа) должен задать вопрос, являются ли эти две единственными жизнеспособными реализациями параллельного мульти читателя/записи данных доступны сегодня?

ответ

3

Мне кажется, что вы слишком сильно усложняете эту проблему. Рассмотрим следующее:

  • Его довольно легко реализовать неизменные версии многих структур данных, особенно деревьев. Неизменяемые структуры данных имеют то преимущество, что в силу неизменности один нить не может изменять коллекцию под другим потоком носа. Неизменяемость = нет условий гонки = нет замков = нет взаимоблокировки. Awesomeness.

    См. Okasaki's Purely Functional Data Structures, в котором реализованы реализации кучи, сбалансированных деревьев, стеков, очередей и некоторых других структур данных ML и Haskell.

  • Нити не видят изменений в неизменяемой структуре данных, выполненной в других потоках. Тем не менее, они могут уведомлять друг друга об изменениях с использованием параллелизма при передаче сообщений.

Замки и мьютексы слишком низкоуровневые, а изменчивое состояние в значительной степени является врагом многопоточного программирования. Если вы думаете о какой-либо проблеме, которую пытаетесь решить в терминах неизменности и передачи сообщений, тогда вам станет легче на 1000 раз.

+1

@Juliet: Проблема с использованием неизменяемой структуры данных заключается в том, что для моего приложения это по существу то же самое, что и использование нескольких считывателей и одного писателя (с точки зрения производительности). Поскольку, если два автора (с неизменной структурой) вставляют один узел, то один из них должен перезапускаться в любом случае, с новым деревом, созданным конкурирующим автором, который первым внес свои изменения. – thr

+0

Существует множество стратегий для решения этой проблемы. В вселенной MapReduce у нас есть одна большая проблема, которая может быть разделена на подвыборы. Например, предположим, что мы хотим найти первые 1000 000 простых чисел, используя пробное подразделение. Мы могли бы запустить 100 дочерних потоков, где первая нить проверяет число 1-999, вторую проверку потоков 1000-1999, следующие тесты 2000-3999 и т. Д. По мере завершения каждого потока он передает результаты в другой поток, который объединяет все отдельные результаты в один результат. – Juliet

+0

Если вы не можете разделить проблему на подзадачи, также работает следующее: основной поток содержит вашу структуру данных. Мастер-поток предоставляет два свойства: Insert и Query, где эти методы изменяют или возвращают структуру данных в ее текущем состоянии соответственно. Детские потоки могут вызывать методы в основном потоке для обновления или запроса структуры данных. Поверхность этой стратегии - это атомарные обновления (и, если они реализованы правильно, транзакционные), и потоки не могут перезаписывать друг друга. Недостатком является то, что дочерние потоки не имеют доступа к структуре данных в режиме реального времени. – Juliet

2

Ну, вы уже определили один я, как правило, предложить - одновременное skiplist, но отсутствие других, чем выше три, я думаю, простой связанный список с пошаговым узлом мьютексами будет работать другие специфические требования:

Каждый узел содержит один элемент (или ссылку) и один простой мьютекс. Я буду использовать Java здесь, потому что это помогает избежать условий гонки вокруг восстановления узла.

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

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

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

Эта структура сортируется (насколько это правильно - я этого не доказал!).

Эта структура, очевидно, позволяет использовать несколько считывателей (по какой-либо причине считыватели никогда не блокируются) и несколько авторов, хотя авторы, пытающиеся манипулировать одной и той же частью списка (например, два вставки потоков, имеющие одну и ту же точку сращивания), будут ждать друг друга.

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

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

В C++ структура более активна, поскольку вы не можете полагаться на GC, чтобы поддерживать узлы вокруг, пока читатели могут смотреть на них, поэтому простая стратегия, позволяющая читателям перемещаться в блокировке - свободный способ не летает, если вы хотите удалить узлы. Вы можете настроить его, заставляя читателей блокировать каждый посещенный узел, но это отстойно как для производительности (очевидной), так и для параллелизма (потому что, хотя вы можете иметь несколько читателей и писателей каким-то основным способом, они никогда не смогут переходить друг к другу, поэтому практический параллелизм сильно ограничен).

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

Теперь, все, что сказало (whew!), Я должен упомянуть, что другое, что «легко рассуждать» об этой структуре кажется почти уступающим параллельному списку пропуска любым материальным способом, за исключением, возможно, немного меньшего использования памяти (может быть). В частности, в нем нет поведения поиска журнала (N). Он не полностью блокирован (в некоторых случаях авторы могут ждать авторов). Я даже не уверен, что гораздо проще рассуждать о том, насколько параллелизм идет, хотя базовая структура проста.

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

+0

@BeeOnRope: Спасибо за замечательный ответ, и хотя я бы хотел дать вам больше, чем один передовой, я все же должен сказать, что идея использовать обычный связанный список с мьютексом для каждого узла приведет к быстрому ухудшению производительности из-за O (n) поиск/обновление/вставка. Мне действительно нужно O (log n) для поиска/вставки/обновления, которое в основном оставляет списки пропуска или бинарные деревья. Я попытался реализовать двоичное дерево множественного чтения/записи с помощью блокировок, но это оказалось невероятно сложным процессом из-за поворотов. – thr

+0

Если вы можете удалить требование сортировки значений (или если отсортированный доступ не так редок, что достаточно полного сортировки при необходимости), хэш-карта с полосатыми замками будет работать очень хорошо, что значительно лучше, чем параллельный список пропусков. Могу ли я спросить, почему параллельный список пропусков не отвечает вашим потребностям? – BeeOnRope

0

Я создал блокирующую структуру данных в F# с аналогичными требованиями для моей работы в последнее время.В частности, это был отсортированный словарь, который сопоставил int ключами с int значениями, в которых значения были счетчиками, а две примитивные операции увеличивали счет, связанный с заданным ключом, и получали массив из текущих пар ключ-значение.

Я реализовал это в F# как значение типа Map<int, int ref> ref, который является изменяемой ссылкой на неизменяемую карту от int ключей изменяемых ссылок на int значений. Читатели одновременно читают ссылку для получения текущей карты, просматривают ее ключ и разыгрывают, чтобы получить соответствующее значение int. Писатели одновременно читают ссылку, просматривают ключ и атомарно увеличивают его, если он существует, или создают новую карту с новой парой ключ-значение и используют CAS для замены корневой ссылки на карту.

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

+0

«один писатель может обновить его безопасно ...» Он говорит, что хочет разрешить нескольким писателям одновременно обновлять структуру. –

+0

@Seun: Вы абсолютно правы.Я понятия не имею, о чем я думал, когда писал свой оригинальный ответ, поэтому заменил его чем-то, что, надеюсь, не бесчувственным. ;-) –