Полностью общий, оптимизированный многопоточный код, как известно, трудно писать.
Самым простым решением, как правило, является защита одновременных изменений с помощью блокировки, как в приведенном вами примере: (bt:with-lock-held (*lock*) (incf *count*))
.Производительность приемлема в большинстве случаев.Я бы рассмотрел только другие варианты ниже, если вы сравните ваш конкретный вариант использования и обнаружите, что он слишком медленный для ваших нужд.
Атомарные операции, поскольку (sb-ext:atomic-incf *count*)
- это примитив очень низкого уровня: очень быстрый, но очень сложно правильно составить в более сложные операции.Если нужные вам функции могут быть сопоставлены один за другим с атомарными операциями, вы можете просто использовать их, и все готово.Но в большинстве случаев вам необходимо составлять элементарные операции для обеспечения более сложных функций - и возникает трудность: вам необходимо глубоко понять архитектуру, которую вы используете, включая (отсутствие) гарантий упорядочения памяти и барьеров памяти.И это чрезвычайно сложный путь.
Моя библиотека STMX предоставляет предположительно интуитивно понятные и простые в составлении примитивы, например, (stmx:atomic (incf *count*))
.Он внутренне использует атомарные операции (если доступны) и инструкции ЦП Intel TSX для транзакционной памяти (только в sbcl x86-64 и только в процессорах Intel, в которых они есть) для оптимизации скорости выполнения.
Он имеет несколько предостережений:
он работает только для типов с поддержкой транзакций, таких как tvar
, tcell
, tcons
, tlist
, tmap
, tfifo
, tchannel
, определены классыс (stmx:transactional (defclass ...))
или структурами, определенными с помощью (stmx:transactional (defstruct ...))
код внутри (stmx:atomic ...)
может быть повторен несколько раз в случае конфликтов между несколькими потоками, поэтому он не должен выполнять ввод-вывод.
обычно он медленнее, чем оптимизированный вручную код, использующий атомарные операции, но его гораздо проще писать, в том числе потому, что он компонуется: (atomic (atomic (foo) (atomic (bar))
- это одна транзакция (не три)и эквивалентно (atomic (foo) (bar))
.
В вашем конкретном случае вы хотите использовать существующие не поточно-ориентированные вызовы библиотеки как (alexandria:deletef x *list*)
в многопоточном коде, т.е.сделать их потокобезопасными.
Если библиотеки делаютЕсли вы не используете и не изменяете глобальные переменные внутри, тогда блокировки и атомарные операции могут быть успешно использованы.Вместо этого STMX можно использовать только в том случае, если вы одновременно изменяете типы с поддержкой транзакций only - могут использоваться предоставляемые библиотекой типы, но они должны рассматриваться как доступные только для чтения параллельным кодом.
Если вместо этогобиблиотеки используют и изменяют глобальные переменные внутри себя, вы гораздо более ограничены, и, вероятно, блокировки - единственное жизнеспособное решение.
PS пожалуйста, отправляйте сообщения о проблемах STMX на https://github.com/cosmos72/stmx/issues @Svante, только что обновленный https://github.com/cosmos72/stmx/issues/14 с ошибкой @davipough в комментариях выше, но точные версии необходимы для устранения проблемы.