Руководство по нитям в SBCL

Материал из wiki.lissyara.su
Перейти к: навигация, поиск

Это перевод руководства по нитям в SBCL

Нити

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 в одном окне не приведёт к неудобному прерыванию работы других слушающих нитей.