#multithreading #printing #common-lisp
#многопоточность #печать #common-lisp
Вопрос:
В одном из сообщений в блоге Тимми Хосе на https://z0ltan.wordpress.com/2016/09/02/basic-concurrency-and-parallelism-in-common-lisp-part-3-concurrency-using-bordeaux-and-sbcl-threads/ он приводит пример неправильного способа печати на верхний уровень изнутри потока (используя в качестве примера потоки Bordeaux, хотя я использую Lparallel):
(defun print-message-top-level-wrong ()
(bt:make-thread
(lambda ()
(format *standard-output* "Hello from thread!")))
nil)
(print-message-top-level-wrong) -> NIL
Объяснение заключается в том, что «Тот же код выполнялся бы нормально, если бы мы не запускали его в отдельном потоке. Что происходит, так это то, что у каждого потока есть свой собственный стек, в котором переменные восстанавливаются. В этом случае, даже для *standard-output*
, которая, будучи глобальной переменной, как мы предполагаем, должна быть доступна для всех потоков, является отскоком внутри каждого потока!»
И это именно то, что происходит, если функция выполняется в Allegro CL. Однако в SBCL функция действительно печатает предполагаемый результат на терминале. Означает ли это, что *standard-output*
не выполняется восстановление в SBCL? В общем, существует ли кроссплатформенный способ печати в *standard-output*
изнутри потока?
В многопоточной ситуации печать на терминал обычно должна быть скоординирована, чтобы избежать потенциальной печати из нескольких потоков одновременно. Но, похоже, нет никаких функций, таких как atomic-format
или atomic-print
доступных. Есть ли простой способ избежать помех при печати при наличии нескольких потоков (при условии, что блокировки / мьютексы слишком дороги для использования для каждой отдельной операции печати)?
Ответ №1:
Если у вас действительно есть глобальная привязка (привязка в глобальной среде), она действительно работает для всех потоков; смотрите документацию для bt:make-thread. Только динамические (повторные) привязки являются локальными для потока. Реализации отличаются тем, как / когда они связывают эти потоки; иногда привязка, которая фактически действует для пользовательских программ, является глобальной, иногда нет.
Мне нравится использовать какую-то очередь или канал для координации вывода, где это необходимо; Я еще не сталкивался с ситуациями, когда накладные расходы на блокировку были чрезмерно высокими.
Возможно, вы могли бы попробовать что-то с оптимистичной блокировкой, но я не знаю, что было сделано для этого по библиотекам (в некоторых реализациях Lisp есть операции CAS, которые можно использовать). Это должно быть ортогонально используемой библиотеке параллелизма.
РЕДАКТИРОВАТЬ: Только что найдено в руководстве по SBCL: sb-concurrency имеет очереди и почтовые ящики без блокировки.
Комментарии:
1. Если я правильно понимаю, я могу поместить глобальную
*standard-output*
в lparallel очередь, а затем использовать ее в любое время в любом потоке, просматривая очередь, чтобы привязать ее к локальной переменной потока, которая отображается как аргумент stream вformat
операторах потока? То есть, даже если два потока одновременно выполняютformat
инструкции на верхнем уровне, один будет ждать, пока другой не завершит работу?2. Верно ли, что если все потоки только читают (без записи) глобальную структуру данных, то операции чтения (без блокировок или атомарных операций) гарантированно безопасны? Например, две
find
операции в разных потоках, сканирующие один и тот же глобальный список одновременно для одних и тех же или разных элементов?3. Все, что просто предоставляет стандартный вывод независимым выводам разных потоков, приведет к промежуточному выводу.
4. Если вы можете гарантировать, что ничто не изменит структуру данных (даже под капотом, например, самооптимизирующиеся структуры данных), чтение это безопасно, очевидно.
5. Можете ли вы указать на пример операции Lisp, которая выглядит как доступная только для чтения, но на самом деле выполняет запись под капотом? (PS: Я не беспокоюсь о таких операциях,
maphash
которые вы можете использовать для записи, если хотите.)