Решением может быть использование атомарных операций . Без блокировки, без переключения контекста, без сна и намного быстрее, чем мьютексы или условные переменные. Атомные операции не являются окончательным решением для всего, но мы создали много поточно-ориентированных версий общих структур данных, используя только атомарные операции. Они очень быстрые.
Атомные операции - это серия простых операций, таких как увеличение, уменьшение или назначение, которые гарантированно выполняются атомарно в многопоточной среде. Если два потока попадают в операцию одновременно, процессор гарантирует, что один поток выполняет операцию одновременно. Атомные операции - это аппаратные инструкции, поэтому они быстрые «Сравнить и поменять» очень полезно для многопоточных структур данных. В нашем тестировании атомарное сравнение и свопирование выполняются примерно с 32-битным целочисленным назначением. Может быть, в 2 раза медленнее. Если учесть, сколько процессоров потребляется мьютексами, атомные операции выполняются бесконечно быстрее.
Это не тривиально, чтобы сделать вращения, чтобы сбалансировать ваше дерево с атомарными операциями, но не невозможно. Я сталкивался с этим требованием в прошлом и обманул, сделав потокобезопасным skiplist , так как с атомарными операциями можно очень просто создать skiplist. Извините, я не могу дать вам копию нашего кода ... моя компания уволит меня, но это достаточно легко сделать самостоятельно.
Как атомарные операции позволяют создавать структуры данных без блокировок, можно визуализировать на примере простого связанного с потоком безопасного списка. Чтобы добавить элемент в глобальный связанный список (_pHead) без использования блокировок. Сначала сохраните копию _pHead, pOld. Я думаю об этих копиях как о «состоянии мира» при выполнении одновременных операций. Затем создайте новый узел pNew и установите для его pNext значение COPY . Затем используйте атомарное «сравнить и поменять», чтобы изменить _pHead на pNew ТОЛЬКО ЕСЛИ pHead ЕЩЕ ПРОСТО. Атомная операция будет успешной, только если _pHead не изменился. Если это не удалось, вернитесь назад, чтобы получить копию нового _pHead, и повторите.
Если операция прошла успешно, остальной мир теперь увидит новую голову. Если поток получил старую голову за наносекунду раньше, этот поток не увидит новый элемент, но список все равно будет безопасным для перебора. Поскольку мы предварительно установили pNext на старую головку, ДО того, как мы добавили наш новый элемент в список, если поток увидит новую головку через наносекунду после того, как мы добавим ее, список будет безопасным для перемещения.
Глобальный материал:
typedef struct _TList {
int data;
struct _TList *pNext;
} TList;
TList *_pHead;
Добавить в список:
TList *pOld, *pNew;
...
// allocate/fill/whatever to make pNew
...
while (1) { // concurrency loop
pOld = _pHead; // copy the state of the world. We operate on the copy
pNew->pNext = pOld; // chain the new node to the current head of recycled items
if (CAS(&_pHead, pOld, pNew)) // switch head of recycled items to new node
break; // success
}
CAS является сокращением для __ sync_bool_compare_and_swap и т.п. Видишь как легко? Нет мьютексов ... нет замков! В том редком случае, когда 2 потока попадают в этот код одновременно, один просто повторяется во второй раз. Мы видим только второй цикл, потому что планировщик меняет поток, находясь в цикле параллелизма. так что это редко и несущественно.
Вещи можно снять с головы связанного списка аналогичным образом. Вы можете атомарно изменить более одного значения, если вы используете объединения и можете использовать до 128-битных атомарных операций. Мы протестировали 128 бит на 32-битной Redhat Linux, и они имеют такую же скорость, что и 32, 64-битные атомные операции.
Вы должны выяснить, как использовать этот тип техники с вашим деревом. Узел дерева b будет иметь два ptrs для дочерних узлов. Вы можете CAS их, чтобы изменить их. Проблема балансировки сложная. Я вижу, как вы можете проанализировать ветвь дерева, прежде чем что-то добавить и сделать копию ветви с определенной точки. Когда вы заканчиваете менять ветку, вы вводите новую CAS. Это будет проблемой для больших веток. Может быть, балансировка может быть сделана «позже», когда потоки не борются за дерево. Может быть, вы можете сделать так, чтобы дерево было доступно для поиска, даже если вы не каскадировали вращение полностью ... другими словами, если поток A добавил узел и рекурсивно вращает узлы, поток b все еще может читать или добавлять узлы. Просто несколько идей. В некоторых случаях мы создаем структуру, которая имеет номера версий или флаги блокировки в 32 битах после 32 битов pNext. Тогда мы используем 64-битный CAS. Возможно, вы могли бы сделать дерево безопасным для чтения в любое время без блокировок, но вам, возможно, придется использовать технику управления версиями для ветви, которая изменяется.
Вот несколько постов, которые я сделал, рассказывающих о преимуществах атомных операций:
Потоки и мьютексы; запирающая часть массива
Эффективный и быстрый способ для аргумента потока
Конфигурация автоматической перезагрузки с помощью pthreads
Преимущества использования условных переменных над мьютексом
манипулирование одним битом
Является ли выделение памяти в linux неблокирующим?