Руководство по нитям в SBCL
Это перевод руководства по нитям в SBCL
Содержание
- 1 Нити
- 2 Базовое использование нити
- 3 Объекты нити
- 4 Создание, присоединение и Уступание нити
- 5 Асинхронные операции
- 6 Прочие операции
- 7 Состояния ошибок
- 8 Специальные переменные
- 9 Атомарные операции
- 10 Поддержка мьютекса
- 11 Семафоры
- 12 Очередь ожидания/переменные состояния
- 13 Барьеры
- 14 Сессии/Отладка
- 15 Сторонние нити
- 16 Реализация (Linux x86/x86-64)
Нити
SBCL поддерживает интерфейс потоков достаточно низкого уровня, что в свою очередь отображает концепцию потоков Операционной Системы и легковесные процессы. Это означает что нити могут использовать аппаратную поддержку на машинах, с несколькими процессорами, но в этом случае Lisp не может контролировать планировщик. Управление потоками находится в пакете SB-THREAD.
Потоки в SBCL, по умолчанию, собираются только в x86[-64] версиях Linux.
Кроме того, есть экспериментальная поддержка на следующих операционных системах: x86[-64] Darwin (Mac OS X), x86[-64] FreeBSD, x86 SunOS (Solaris) и PPC Linux. На этих платформах, поддержку нитей необходимо указывать при сборке SBCL, за дополнительной информацией смотрите пункт INSTALL в исходных текстах SBCL.
Базовое использование нити
(make-thread (lambda () (write-line "Hello, world")))
Объекты нити
— Структура: thread [sb-thread]
- Список приоритета класса: thread, structure-object, t
- Тип нити. Не следует работать на прямую со структурой нити, поскольку она может измениться в будущем.
— Переменная: *current-thread* [sb-thread]
- Эта переменная указывает на текущую нить.
— Функция: list-all-threads [sb-thread]
- Возвращает список живых нитей. Помните, что возвращаемое значение может быть устаревшим, поскольку во время отработки функции может возникнуть новая нить, а старая нить может завершиться в любое время.
— Функция: thread-alive-p [sb-thread] thread
- Возвращает t в случае живой нити. Помните, что возвращаемое значение может устареть, поскольку нить может завершиться в любое время.
— Функция: thread-name [sb-thread] instance
- Возвращается в случае присутствия debug-block в коде.
Создание, присоединение и Уступание нити
— Функция: make-thread [sb-thread] function &key name arguments
- Создать новую нить под названием name, запускающую функцию function с заданным списком обозначающим аргументы (по умолчанию, аргументов нет). При завершении функции - завершается и нить. Возвращаемые значения функции function сохраняются и могут возвращаться с помощью join-thread.
— Функция: thread-yield [sb-thread]
- Передаёт вычислительную мощь процессора другим нитям.
— Функция: join-thread [sb-thread] thread &key default timeout
- Приостанавливает текущую нить пока не завершится нить thread. Возвращает результирующие значения функции нити.
- Если нить не завершилась нормально за секунды заданные в timeout, то возвращается default. Если default не задан, вызывается сигнал join-thread-error.
- Замечание: Соглашение о возвращаемом значении в случае таймаута является экспериментальным и может измениться.
— Функция: thread-yield [sb-thread]
- Уступить процессор другим нитям.
Асинхронные операции
— Функция: interrupt-thread [sb-thread] thread function
- Прерывание нити и запуск его функции.
- Прерывание является асинхронным и может произойти где угодно, за исключением разделов защищённых с помощью использования sb-sys:without-interrupts.
- Функция function вызывается с отключёнными прерываниями, через sb-sys:allow-with-interrupts. Такие функции как grab-mutex могут попытаться включить прерывания внутренними механизмами, в большинстве случаев функция function должна использовать либо sb-sys:with-interrupts для разрешения вложенных прерываний или sb-sys:without-interrupts для окончательного предотвращения прерываний.
- Выполнение множественных прерываний нитью выполняется в порядке их приёма - первый пришёл, первым и уйдёт.
- Отсюда можно сделать вывод о том, что использовать interrupt-thread надо осторожно и с осознанием того, что Вы делаете, особенно в производственной среде. Главная рекомендация - это ограниченное использование interrupt-thread в целях интерактивной отладки. В производственной среде нужно полностью отказаться от interrupt-thread, поскольку очень сложно правильно использовать эту функцию.
- Вот что Вам нужно знать с учётом вышеприведённых оговорок:
- Если вызов функции function приводит к не-локальной передаче управления (то есть unwind), то необходимо выполнить все нормальные формы очистки.
- Однако если прерывание произойдёт в форме очистки unwind-protect, то очистки не произойдёт.
- sbcl пытается сохранять своё внутреннее состояние asynch-unwind-safe, но не будет разумным полностью полагаться на это в библиотеках третьих лиц: функция вызывающая функцию asynch-unwind-safe не является безопасной.
- Это означает что для достижения настоящей безопасности весь стек вызовов должен находиться в asynch-unwind-safe.
- В дополнение к asynch-unwind-safety Вы должны рассмотреть вопрос о повторном входе. interrupt-thread может стать причиной нарушения нормального рекурсивного вызова какой-либо функции. (Необходимо рассмотрение связанных специальных переменных, значений глобальных переменных и т.д.)
- С учётом этих двух замечаний функцию interrupt-thread можно применять для очень узкопрофильных целей. Одна из них - это интерактивная разработка при принудительном вызове отладчика для проверки состояния нити:
(interrupt-thread thread #'break)
- Одним словом: будьте осторожны.
— Функция: terminate-thread [sb-thread] thread
- Убить нить идентифицированный как thread, через прерывание этой нити и вызов sb-ext:abort-thread с :allow-exit t.
- Unwind вызывается асинхронным terminate-thread, это означает что нить выполняет
(let (foo) (unwind-protect (progn (setf foo (get-foo)) (work-on-foo foo)) (when foo ;; Прерывание произошедшее внутри кода очистки, ;; приводящие к очистке текущим UNWIND-PROTECT ;; будут отброшены. (release-foo foo))))
- в результате работы этого кода будет пропущен вызов release-foo несмотря на то, что get-foo вернёт true в случае возникновения прерывания внутри пункта очистки, например во время выполнения release-foo.
- Таким образом, для написания асинхронного unwind-protect Вы должны использовать without-interrupts:
(let (foo) (sb-sys:without-interrupts (unwind-protect (progn (setf foo (sb-sys:allow-with-interrupts (get-foo))) (sb-sys:with-local-interrupts (work-on-foo foo))) (when foo (release-foo foo)))))
- Поскольку множество библиотеку использующие unwind-protect не используют without-interrupts, необходимо помнить что неизвестный код может не безопасно завершится с помощью terminate-thread.
Прочие операции
— Функция: symbol-value-in-thread [sb-thread] symbol thread &optional errorp
- Возвращает локальное значение символа symbol в нити thread и второе значение t в случае успеха.
- Если значение не было получено (поскольку нить может завершить работу или не имеет локальной привязки по ИМЕНИ) и errorp установлено в true то генерируются сигналы об ошибках типа symbol-value-in-thread-error; если errorp установлен в false то главным и дополнительным значением возвращается nil.
- Возможно использование совместно с setf для изменения значения thread-local символа symbol.
- Функция symbol-value-in-thread главным образом предназначена для отладки и а не механизма меж нитевой коммуникации.
Состояния ошибок
— Состояние: thread-error [sb-thread]
- Список приоритета класса: thread-error, error, serious-condition, condition, t
- Состояния типа thread-error сигнализируются при не корректном выполнении операций нити. Нить-нарушитель инициализируется с помощью аргумента :thread и считывается функцией thread-error-thread.
— Функция: thread-error-thread [sb-thread] condition
- Возвращается нить нарушитель содержащая thread-error.
— Состояние: interrupt-thread-error [sb-thread]
- Список приоритета класса: interrupt-thread-error, thread-error, error, serious-condition, condition, t
- При неудачном прерывании нити, поскольку нить уже завершила свою работу. Доступ к нити-нарушителю можно получить через использование thread-error-thread.
— Состояние: join-thread-error [sb-thread]
- Список приоритета класса: join-thread-error, thread-error, error, serious-condition, condition, t
- Сигнализируется при ненормальном завершении работы присоединённой нити. Доступ к нити-нарушителю можно получить через thread-error-thread.
Специальные переменные
Взаимодействие специальных переменных с несколькими нитями, как и следовало ожидать, является похожим на другие реализации.
- глобальные специальные переменные видны для всех нитей;
- привязки (например, с помощью LET) являются локальными для нити;
- нити не наследуют динамические привязки с родительской нити
Последнее означает следующее:
(defparameter *x* 0) (let ((*x* 1)) (sb-thread:make-thread (lambda () (print *x*))))
напечатает 0 а не 1 как в версии 0.9.6.
Атомарные операции
SBCL предоставляет несколько специальных атомных операций, особенно удобных для реализации неблокируемых алгоритмов.
— Макрос : atomic-decf [sb-ext] place &optional diff
- Атомарно декрементирует place на diff, и возвращает значение place до инкремента.
- Декрементация осуществляется через модулярную арифметику с использованием переменных с размером слова (word): на 32-х битных платформах макрос atomic-decf с аргументом #x0 вернёт #xFFFFFFFF и сохранит в place.
- place должен быть формой аксессора, где car является именем defstruct аксессора с объявленным типом (32 без-знаковых байта) на 32-х битных платформах и (64 без-знаковых байта) на 64-х битной платформе или aref от (SIMPLE-ARRAY sb-ext:word (*) - для этих целей может использоваться тип sb-ext:word.
- diff по умолчанию установлен в 1 и может быть (32 знаковых байта) на 32-х битных платформах и (64 знаковых байта) на 64-х битной платформе.
- экспериментально: Интерфейс будет меняться.
— Макрос: atomic-incf [sb-ext] place &optional diff
- Атомарно инкрементирует place на diff и возвращает значение place до инкремента.
- Инкремент осуществляется через модулярную арифметику с использованием переменных с размером слова (word): на 32-х битных платформых макрос atomic-incf с аргументом #xFFFFFFFF вернёт #x0 и сохранит в place.
- place должен быть формой аксессора, где car является именем defstruct аксессора с объявленным типом (32 без-знаковых байта) на 32-х битных платформах и (64 без-знаковых байта) на 64-х битной платформе или aref от (SIMPLE-ARRAY sb-ext:word (*) - для этих целей может использоваться тип sb-ext:word.
- diff по умолчанию установлен в 1 и может быть (32 знаковых байта) на 32-х битных платформах и (64 знаковых байта) на 64-х битной платформе.
- экспериментально: Интерфейс будет меняться.
— Макрос: compare-and-swap [sb-ext] place old new
- Атомарно сохраняет new в place в случае когда old соответствует текущему значению place. Два значения считаются соответствующими, тогда, когда по отношению к ним выполняется eq. Предыдущее значение place возвращается в следующих случаях: если возвращаемое значение равно eq к old, была произведена замена.
- place должен представлять форму аксессора в котором car является одним из следующих вещей:
- car, cdr, first, rest, svref, symbol-plist, symbol-value, svref, slot-value sb-mop:standard-instance-access, sb-mop:funcallable-standard-instance-access,
- или defstruct имя созданного аксессора для слота, с одним из объявленных типов: fixnum или t. Результат будет не известен в том случае, когда объявленный тип слота будет отличен от fixnum или t.
- В случае пустого slot-value, slot-unbound будет вызываться при выполнении eq между old и sb-pcl:+slot-unbound+ и sb-pcl:+slot-unbound+ будет возвращать new присоединённый к слоту.
- Дополнительно к вышесказанному, результаты не определены если есть применимый метод к sb-mop:slot-value-using-class, (setf sb-mop:slot-value-using-class) или sb-mop:slot-boundp-using-class.
- экспериментально: Интерфейс объекта может меняться.
Протокол CAS
- Наша функция compare-and-swap является расширяемой по протоколу, подобному setf:
— Макрос: cas [sb-ext] place old new env
- Синоним для compare-and-swap.
- Дополнительно возможно использование defun, defgeneric, defmethod, flet и labels для определения CAS-функций по аналогии с SETF-функциями:
(defvar *foo* nil) (defun (cas foo) (old new) (cas (symbol-value '*foo*) old new))
- Первым аргументом cas функции будет старое значение, а вторым аргументом - новое значение. Заметим, что система не обеспечивает автоматической атомарности для cas функций и не может проверить, что они атомарны: проверка атомарности лежит на реализаторе cas функции.
- экспериментально: Интерфейс субъекта может меняться.
— Макрос: define-cas-expander [sb-ext] accessor lambda-list &body body
- Аналогично define-setf-expander. Описывает CAS-расширение для accessorа. body должно возвращать шесть значений, описанных в get-cas-expansion.
- Заметим, что система не обеспечивает автоматической атомарности для cas функций и не может проверить, что они атомарны: проверка атомарности лежит на реализаторе cas функции.
- экспериментально: Интерфейс субъекта может меняться.
— Макрос: defcas [sb-ext] form accessor lambda-list function &optional docstring
- Аналогично короткой форме defsetf. Описывает функцию ответственную за сравнение-и-замену (compare-and-swap) на местах, доступных через accessor. lambda-list должен соответствовать лямбда-списку аксессора.
- Заметим, что система не обеспечивает автоматическую атомарность для cas расширений от макроса defcas, и не может проверить атомарность: поскольку эта задача лежит на пользователе defcas.
- экспериментально: Интерфейс субъекта может меняться.
— Функция: get-cas-expansion [sb-ext] place &optional environment
- Аналог get-setf-expansion. Возвращаются следующие шесть значений:
- список временных переменных
- список значений форм (value-forms), результаты должны быть связаны
- временное значение для старого значения place
- временное значение для нового значения place
- форма использующая вышеупомянутые временные переменные для операции compare-and-swap над place
- форма использующая вышеупомянутые временные переменные для операции чтения place
Пример:
(get-cas-expansion '(car x)) ; => (#:CONS871), (X), #:OLD872, #:NEW873, ; (SB-KERNEL:%COMPARE-AND-SWAP-CAR #:CONS871 #:OLD872 :NEW873). ; (CAR #:CONS871) (defmacro my-atomic-incf (place &optional (delta 1) &environment env) (multiple-value-bind (vars vals old new cas-form read-form) (get-cas-expansion place env) (let ((delta-value (gensym "DELTA"))) `(let* (,@(mapcar 'list vars vals) (,old ,read-form) (,delta-value ,delta) (,new (+ ,old ,delta-value))) (loop until (eq ,old (setf ,old ,cas-form)) do (setf ,new (+ ,old ,delta-value))) ,new))))
- экспериментально: Интерфейс субъекта может меняться.
Поддержка мьютекса
Мьютексы используются для управления доступом к общим ресурсам. Только одна нить может захватить мьютекс, остальные нити будут засыпать в ожидании освобождения мьютекса. Нити будут просыпаться в том порядке, в котором они уснули.
Нет какого-либо специального таймаута для получения мьютекса, но существует макрос WITH-TIMEOUT (порождающий состояние TIMEOUT каждые n секунд) который можно использовать для реализации ограниченного ожидания.
(defpackage :demo (:use "CL" "SB-THREAD" "SB-EXT")) (in-package :demo) (defvar *a-mutex* (make-mutex :name "my lock")) (defun thread-fn () (format t "Thread ~A running ~%" *current-thread*) (with-mutex (*a-mutex*) (format t "Thread ~A got the lock~%" *current-thread*) (sleep (random 5))) (format t "Thread ~A dropped lock, dying now~%" *current-thread*)) (make-thread #'thread-fn) (make-thread #'thread-fn)
— Структура: mutex [sb-thread]
- Список приоритета класса: mutex, structure-object, t
- Тип мьютекса.
— Функция: make-mutex [sb-thread] &key name %owner state
- Создать мьютекс.
— Функция: mutex-name [sb-thread] instance
- Возвращается при наличии debug-block в коде.
— Функция: mutex-value [sb-thread] mutex
- Текущий владелец мьютекса, nil если мьютекс свободен. Возможно возвращение устаревших данных, используйте mutex-owner взамен этой функции.
— Функция: grab-mutex [sb-thread] mutex &key waitp timeout
- Получает мьютекс для текущей нити. Если waitp установлен в true (по умолчанию) и мьютекс в данный момент не доступен, проиходит засыпания до тех пор, пока данный мьютекс освободится.
- Задание timeout определяет количество секунд, на протяжении которых grab-mutex будет пытаться захватить мьютекс в данном случае.
- Если grab-mutex возвращает t, то это означает что получение блокировки было успешным. В случае когда waitp равен nil или истёк timeout, grab-mutex может вернуть nil, что является сигналом о том, что grab-mutex не получил блокировку мьютекса.
- Примечания:
- grab-mutex не производит безопасных прерываний. Правильный способ вызова этой функции:
(WITHOUT-INTERRUPTS ... (allow-with-interrupts (grab-mutex ...)) ...)
- without-interrupts нужен для избежания прерывания раскручивания вызова пока мьютекс находится в нестабильном состоянии и allow-with-interrupts позволяют вызову уходить в спячку.
- (grab-mutex <mutex> :timeout 0.0) отличается от (grab-mutex <mutex> :waitp nil) тем, что первый может сигнализировать о наступлении deadline-timeout если уже случился глобальный deadline при входе в grab-mutex.
- В будущем взаимодействие grab-mutex и deadline может изменится.
- Рекомендуется использовать with-mutex взамен непосредственного вызова grab-mutex.
— Функция: release-mutex [sb-thread] mutex &key if-not-owner
- Освобождение мьютекса путём установки его в nil. Пробуждает нити ожидавшие этот мьютекс.
- release-mutex - это не безопасное прерывание: прерывания должны отключать вызовы к нему.
- Если текущая нить не является хозяином мьютекса, то нить возвращается без выполнения каких-либо действий (если if-not-owner установлен в :PUNT), сигналов предупреждения (если if-not-owner установлен в :WARN) и в любом случае освобождает мьютекс (если if-not-owner установлен в :FORCE).
— Макрос: with-mutex [sb-thread] (mutex &key value wait-p) &body body
- Получает мьютекс mutex для динамической области body, устанавливая его в значение value или в некоторое подходящее значение при value равном nil. Если wait-p установлен в не-NIL и мьютекс используется в данный момент, то осуществляется засыпание до тех пор, пока мьютекс не станет свободным.
— Макрос: with-recursive-lock [sb-thread] (mutex) &body body
- Получает mutex для динамической области body. Внутри этой области используются дальнейшая рекурсивная блокировка мьютекса. Разрешается смешанное использование with-mutex и with-recursive-lock для мьютекса, при условии использования значения по умолчанию.
— Функция: get-mutex [sb-thread] mutex &optional new-owner waitp timeout
- Устарела в связи с появлением grab-mutex.
Семафоры
Семафоры - это такая полезная вещь, которая используется для наблюдения за количественными ресурсами, например сообщения в очереди, и засыпают при исчерпании ресурсов.
— Структура: semaphore [sb-thread]
- Список приоритетов класса: semaphore, structure-object, t
- Тип семафора. Тот факт, что семафор (semaphore) - это структурный-объект (structure-object) следует рассматривать как деталь реализации и учитывать что она может подвергнуться изменениям в будущем.
— Функция: make-semaphore [sb-thread] &key name count
- Создать семафор с заданным числом (count) и именем (name).
— Функция: signal-semaphore [sb-thread] semaphore &optional n
- Увеличить число семафора (semaphore) на n. Если есть нити ожидающие этот семафор, то n из них будут пробуждены.
— Функция: wait-on-semaphore [sb-thread] semaphore &key timeout notification
- Декрементировать число семафора (semaphore) в случае когда число не отрицательное. В противном случае заблокировать пока семафор не декрементируется. При благополучном выполнении возвращает t.
- Если задан таймаут (timeout) - то это означает максимальное число секунд для ожидания. Если число не получилось декрементировать за заданное время, то возвращается nil без уменьшения числа.
- Если задано уведомление (notification), то это должен быть объект semaphore-notification чей статус semaphore-notification-status установлен в nil. Если функция wait-on-semaphore отработала успешно и уменьшила число, то статус semaphore-notification-status устанавливается в t.
— Функция: try-semaphore [sb-thread] semaphore &optional n notification
- Пытается уменьшить число семафора (semaphore) на n. Если число стало отрицательным, то возвращается nil, в противном случае возвращается true.
- Если задано уведомление (notification), то это должен быть объект semaphore-notification чей статус semaphore-notification-status установлен в nil. Если число уменьшено, статус устанавливается в t.
— Функция: semaphore-count [sb-thread] instance
- Возвращает текущее число экземпляров (instance) семафора.
— Функция: semaphore-name [sb-thread] instance
- Возвращается при наличии в коде debug-block.
— Структура: semaphore-notification [sb-thread]
- Список приоритетов класса: semaphore-notification, structure-object, t
- Объект уведомления семафора. Может передаваться к функциям wait-on-semaphore и try-semaphore как аргумент :notification. Невозможно предсказать последствия использования таких объектов уведомлений при работе нескольких нитей одновременно.
— Функция: make-semaphore-notification [sb-thread]
- Конструктор для объектов semaphore-notification. semaphore-notification-status устанавливается в nil.
— Функция: semaphore-notification-status [sb-thread] semaphore-notification
- Возвращает t если с использованием semaphore-notication благополучно выполнилась wait-on-semaphore или try-semaphore при создании объекта уведомления или его очистки.
— Функция: clear-semaphore-notification [sb-thread] semaphore-notification
- Сбрасывает объект semaphore-notification для использования с другим вызовом wait-on-semaphore или try-semaphore.
Очередь ожидания/переменные состояния
Базируется на принципе переменных состояния POSIX, и поэтому конфликтуют с CL именами. Применяется для проверки состояний и ожидания, пока это состояние не наступит. На пример: у Вас есть общая очередь, процесс записи проверяет на условие "очередь пустая" и один или более считывателей, которые должны знать о том, что "очередь не пустая". На словах выглядит просто, но на деле удивительно просто зайти в тупик при неожиданном запуске другого процесса.
Существует три компонента:
- само состояние (не присутствует в коде)
- переменная состояния (так же известная как очередь ожидания (waitqueue)), являющееся прокси для него
- блокировка для проверки условия
Важные вещи, которые не следует забывать:
- при вызове condition-wait, вы должны приостановить мьютекс. condition-wait перестанет работать с мьютексом, пока он (мьютекс) на ходится в режиме ожидания, и получит мьютекс после завершения работы по каким-либо причинам;
- кроме того, Вы должны переводить мьютекс в режим ожидания при вызовах condition-notify;
- процесс может вернуться из condition-wait по нескольким обстоятельствам: нет никаких гарантий, что нижележащее состояние будет истинно. Вы должны проверять на готовность ресурсы, что бы Вы не делали.
(defvar *buffer-queue* (make-waitqueue)) (defvar *buffer-lock* (make-mutex :name "buffer lock")) (defvar *buffer* (list nil)) (defun reader () (with-mutex (*buffer-lock*) (loop (condition-wait *buffer-queue* *buffer-lock*) (loop (unless *buffer* (return)) (let ((head (car *buffer*))) (setf *buffer* (cdr *buffer*)) (format t "reader ~A woke, read ~A~%" *current-thread* head)))))) (defun writer () (loop (sleep (random 5)) (with-mutex (*buffer-lock*) (let ((el (intern (string (code-char (+ (char-code #\A) (random 26))))))) (setf *buffer* (cons el *buffer*))) (condition-notify *buffer-queue*)))) (make-thread #'writer) (make-thread #'reader) (make-thread #'reader)
— Структура: waitqueue [sb-thread]
- Список приоритетов класса: waitqueue, structure-object, t
- Тип очереди ожидания (Waitqueue)
— Функция: make-waitqueue [sb-thread] &key name
- Создать очередь ожидания (waitqueue).
— Функция: waitqueue-name [sb-thread] instance
- Возвращает debug-lock, присутствующий где-либо в коде.
— Функция: condition-wait [sb-thread] queue mutex &key timeout
- Атомарное освобождение мьютекса mutex и начало ожидания очереди queue до тех пор, пока не проснётся нить использующая в этой очереди либо condition-notify либо condition-broadcast, и здесь мы заново получаем мьютекс mutex и возвращаем t.
- Возможны ложные пробуждения.
- Задание таймаута timeout означает максимальное число секунд ожидания, включающее в себя время пробуждения и время получения мьютекса mutex. Если процесс пробуждение и повторного получения мьютекса не уложился в заданное время - возвращается nil без получения мьютекса.
- Раскручивание condition-wait может происходить как мьютексом, так и без него.
- Важно: По причине возврата condition-wait без condition-notify корректным кодом является использование condition-wait в цикле вокруг вызова, с проверкой ассоциированных данных:
(defvar *data* nil) (defvar *queue* (make-waitqueue)) (defvar *lock* (make-mutex)) ;; Потребитель (defun pop-data (&optional timeout) (with-mutex (*lock*) (loop until *data* do (or (condition-wait *queue* *lock* :timeout timeout) ;; Lock not held, must unwind without touching *data*. (return-from pop-data nil))) (pop *data*))) ;; Поставщик (defun push-data (data) (with-mutex (*lock*) (push data *data*) (condition-notify *queue*)))
— Функция: condition-notify [sb-thread] queue &optional n
- Уведомить n нитей, которые находятся в очереди queue.
- важно: Некоторые мьютексы, при использовании соответствующего condition-wait, должны приостанавливаться при этом вызове.
— Функция: condition-broadcast [sb-thread] queue
- Уведомить все нити в очереди queue.
- важно: Некоторые мьютексы, при использовании соответствующего condition-wait, должны приостанавливаться при этом вызове.
Барьеры
Они основаны на устройстве барьеров ядра Linux, которое в свою очередь базируется на модели памяти процессора Alpha. Они (барьеры) в настоящее время реализованы для систем x86, x86-64, PPC и ведут себя как барьеры компилятора на всех остальных процессорах.
В дополнение к явному использованию макроса sb-thread:barrier, следующие функции и макросы можно использовать как барьеры :memory:
- sb-ext:atomic-decf и sb-ext:atomic-incf.
- sb-ext:compare-and-swap.
- sb-thread:get-mutex, sb-thread:release-mutex, sb-thread:with-mutex и sb-thread:with-recursive-lock.
- sb-thread:signal-semaphore, sb-thread:try-semaphore и sb-thread:wait-on-semaphore.
- sb-thread:condition-wait, sb-thread:condition-notify и sb-thread:condition-broadcast.
— Макрос: barrier [sb-thread] (kind) &body forms
- Вставляет барьер в код потока, предотвращая некоторые виды изменения порядка.
- Вид kind может быть следующим:
- :compiler
- Барьер предотвращает изменение порядка доступа к памяти компилятором.
- :memory
- Барьер предотвращает изменение порядка доступа к памяти процессором.
- :read
- Барьер предотвращает изменение порядка доступа чтения к памяти процессором.
- :write
- Барьер предотвращает изменение порядка доступа записи к памяти процессором.
- :data-dependency
- Барьер предотвращает изменение порядка доступа чтения к зависимой памяти процессором (требуется производить операцию чтения перед барьером для завершения выполнения чтения после барьера). Это слабая форма барьера :read.
- Форма forms - это не явные progn, вычисляемые до барьера. Барьер barrier возвращает значение последней формы в forms.
- Настоятельно рекомендуется прочесть файл "memory-barriers.txt" из состава ядра Linux для тех, кто хочет программировать на этом уровне.
Сессии/Отладка
Если пользователь работает с несколькими представлениями на некотором Lisp образе (на пример: использует несколько терминалов, оконных систем или сетевой доступ), то в этом случае устанавливается несколько сессий, каждая из которых имеет свой собственный набор переднего плана/фоновых/остановленных нитей. Нить, которая хочет создать новую сессию, может использовать sb-thread:with-new-session для удаления себя из текущей сессии (которую разделяет с родителем и прочими нитями) и создать новую нить. # Также можете почитать sb-thread:make-listener-thread.
В течении одной сессии, нити могут соревноваться между собой за внимание пользователя. Нить может быть в одном из трёх состояний: переднего плана, в фоне или остановленной. Когда фоновый процесс хочет передать что-либо в REPL или перейти в отладчик, он остановится и напечатает сообщение об останове. Пользователь по собственному желанию может перейти в эту нить и выяснить что ей надо. Если фоновая нить входит в отладчик, с выбором перезагрузки она будет возвращена в фон перед продолжением работы. Управление поведением нитей ввода производится с помощью вызовов sb-thread:get-foreground (этот вызов может блокироваться) и sb-thread:release-foreground.
Сторонние нити
Прямой вызов pthread_create (взамен MAKE-THREAD) создаёт нити, о которых SBCL ничего не знает, эти нити называются сторонними. В настоящее время запуск Lisp кода в таких нитях не возможен. Это означает что не будут работать обработчики сигналов Lisp'а. Лучшим решением является запуск сторонних нитей с заблокированными сигналами, но следует учесть тот факт, что библиотеки от третьих лиц сами могут создавать нити и такой подход не всегда будет срабатывать. В качестве обходного пути можно использовать следующий приём: после получения сигнала на сторонней нити, SBCL изменит sigmask нити для блокирования всех сигналов управляемых сигналов и отправит сигнал в текущий процесс и впоследствии отправит сигнал в не блокирующую его нить - то есть в Lisp нить.
Трюк с повторным сигнализированием нельзя использовать с сихронными сигналами (SIGSEGV и иже с ним), будьте осторожны и не включите их ненароком. Повторное сигнализирование синхронно включаемых сигналов - это тема для --lose-on-corruption, смотрите Опции Времени Выполнения.
Реализация (Linux x86/x86-64)
Реализация нитей связана с использованием pthreads и таких Linux специфичных вещей как futex.
На платформе x86 локальная привязка специальных переменных каждого потока создаётся через регистрацию сегмента %fs в точке хранения каждой нити. Что в свою очередь может приводить к интересным последствиям, например: при связи с чужим кодом, ожидающим или создающим нити или при несовместимом использовании %fs библиотекой нитей. На платформе x86-64 эту роль играет регистр r12.
Очереди требуют доступного системного вызова sys_futex(): это основа для требования NPTL. Во время выполнения программы мы производим проверку существования этого системного вызова.
Сборка мусора осуществляется с помощью Conservative Generational GC. Распределение производится в небольшой области (как правило 8k): к каждой нити относится своя собственная область, поэтому работа производится без перерывов. Однако, при заполнении одной области и распределении другой области должна производиться блокировка и если требуется сборка мусора, все процессы должны остановиться. Достигается это через отправку сигналов к процессам, и тут может возникнуть интересный случай: прерывание сигнала системным вызовом. Со стороны интерфейса потока предполагается что требуемый системный вызов произведёт корректную перезагрузку, но это может произойти при выполнении других блокировочных вызовов, в том числе со стороны кода удалённой библиотеки.
Большое количество SBCL библиотеки не было проверено на безопасность работы нитей. Некоторые, явно небезопасные участки обвязаны блокировками, что в свою очередь ограничивает параллельную работу с такими вещами как компилирование и загрузка fasl. В этом направлении ведутся работы.
Новая нить, по умолчанию, создаётся в том же процессе POSIX группы и сессии что и родительская нить. Что в свою очередь оказывает влияние на обработку прерываний от клавиатуры: нажатие на вашем терминале клавиши прерывания (обычно Control-C) вызовет прерывание всех процессов из группы переднего плана, включая Lisp нити, которые SBCL считает "фоновыми". Это нежелательно, поэтому фоновые нити переводятся в игнорирование сигнала SIGINT.
sb-thread:make-listener-thread в дополнение к созданию новой Lisp сессии создаст новую POSIX сессию, и нажатие Control-C в одном окне не приведёт к неудобному прерыванию работы других слушающих нитей.