Запись в Python из нескольких потоков

У меня есть модуль log.py, который используется, по крайней мере, в двух других модулях (server.py и device.py).

Он имеет эти глобальные переменные:

fileLogger = logging.getLogger()
fileLogger.setLevel(logging.DEBUG)
consoleLogger = logging.getLogger()
consoleLogger.setLevel(logging.DEBUG)

file_logging_level_switch = {
    'debug':    fileLogger.debug,
    'info':     fileLogger.info,
    'warning':  fileLogger.warning,
    'error':    fileLogger.error,
    'critical': fileLogger.critical
}

console_logging_level_switch = {
    'debug':    consoleLogger.debug,
    'info':     consoleLogger.info,
    'warning':  consoleLogger.warning,
    'error':    consoleLogger.error,
    'critical': consoleLogger.critical
}

Он имеет две функции:

def LoggingInit( logPath, logFile, html=True ):
    global fileLogger
    global consoleLogger

    logFormatStr = "[%(asctime)s %(threadName)s, %(levelname)s] %(message)s"
    consoleFormatStr = "[%(threadName)s, %(levelname)s] %(message)s"

    if html:
        logFormatStr = "<p>" + logFormatStr + "</p>"

    # File Handler for log file
    logFormatter = logging.Formatter(logFormatStr)
    fileHandler = logging.FileHandler( 
        "{0}{1}.html".format( logPath, logFile ))
    fileHandler.setFormatter( logFormatter )
    fileLogger.addHandler( fileHandler )

    # Stream Handler for stdout, stderr
    consoleFormatter = logging.Formatter(consoleFormatStr)
    consoleHandler = logging.StreamHandler() 
    consoleHandler.setFormatter( consoleFormatter )
    consoleLogger.addHandler( consoleHandler )

И:

def WriteLog( string, print_screen=True, remove_newlines=True, 
        level='debug' ):

    if remove_newlines:
        string = string.replace('\r', '').replace('\n', ' ')

    if print_screen:
        console_logging_level_switch[level](string)

    file_logging_level_switch[level](string)

Я вызываю LoggingInit из server.py, который инициализирует файловые и консольные журналы. Затем я вызываю WriteLog со всех сторон, поэтому несколько потоков обращаются к fileLogger и consoleLogger.

Нужна ли дополнительная защита для моего файла журнала? В документации указано, что обработчики потоков обрабатываются обработчиком.

Ответы

Ответ 1

Хорошей новостью является то, что вам не нужно делать ничего лишнего для безопасности потоков, и вам не нужно ничего лишнего или что-то почти тривиальное для чистого выключения. Я расскажу подробности позже.

Плохая новость заключается в том, что ваш код имеет серьезную проблему даже до того, как вы дойдете до этой точки: fileLogger и consoleLogger - это один и тот же объект. Из документация для getLogger():

Верните регистратор с указанным именем или, если имя не указано, верните регистратор, который является корневым регистратором иерархии.

Итак, вы получаете корневой журнал и сохраняете его как fileLogger, а затем получаете корневой журнал и сохраняете его как consoleLogger. Итак, в LoggingInit вы инициализируете fileLogger, затем повторно инициализируете один и тот же объект под другим именем с разными значениями.

Вы можете добавить несколько обработчиков в один и тот же журнал - и, поскольку единственная инициализация, которую вы фактически выполняете для каждого, - это addHandler, ваш код будет сортировать работу по назначению, но только случайно. И только вроде. Вы получите две копии каждого сообщения в обоих журналах, если вы пройдете print_screen=True, и вы получите копии в консоли, даже если вы пройдете print_screen=False.

На самом деле нет причин для глобальных переменных; вся точка getLogger() заключается в том, что вы можете вызывать ее каждый раз, когда вам это нужно, и получать глобальный корневой журнал, поэтому вам не нужно его хранить где-нибудь.


Более незначительная проблема заключается в том, что вы не избегаете текста, который вы вставляете в HTML. В какой-то момент вы попытаетесь записать строку "a < b" и попадете в проблему.

Менее серьезно, последовательность тегов <p>, которая не находится внутри <body> внутри <html>, не является допустимым HTML-документом. Но множество зрителей позаботятся об этом автоматически, или вы можете отправить свои журналы тривиально перед их отображением. Но если вы действительно хотите, чтобы это было правильно, вам нужно подклассом FileHandler и добавьте __init__ заголовок, если ему задан пустой файл, и удалите нижний колонтитул, если он есть, а затем добавьте нижний колонтитул close.


Возвращаясь к вашему актуальному вопросу:

Вам не нужна дополнительная блокировка. Если обработчик правильно реализует createLock, acquire и release (и он вызывается на платформе с потоками), система ведения журнала автоматически будет обязательно получать блокировку, когда это необходимо, чтобы каждое сообщение регистрировалось атомарно.

Насколько я знаю, в документации прямо не сказано, что StreamHandler и FileHandler реализуют эти методы, это сильно подразумевает (текст, который вы упомянули в вопросе, гласит: "Модуль протоколирования предназначен для потокобезопасности без какой-либо специальной работы, которую должны выполнять его клиенты" и т.д.). И вы можете посмотреть источник для своей реализации (например, CPython 3.3) и увидеть, что они оба наследуют правильно реализованные методы из logging.Handler.


Аналогично, если обработчик корректно реализует flush и close, механизм регистрации будет корректно завершен при нормальном отключении.

Здесь документация объясняет, что StreamHandler.flush(), FileHandler.flush() и FileHandler.close(). В основном это то, что вы ожидаете, за исключением того, что StreamHandler.close() - это не-op, что означает, что окончательные сообщения журнала на консоли могут потеряться. Из документов:

Обратите внимание, что метод close() наследуется от Handler и поэтому не выводит, поэтому может потребоваться явный вызов flush().

Если это имеет значение для вас, и вы хотите его исправить, вам нужно сделать что-то вроде этого:

class ClosingStreamHandler(logging.StreamHandler):
    def close(self):
        self.flush()
        super().close()

И затем используйте ClosingStreamHandler() вместо StreamHandler().

FileHandler не имеет такой проблемы.


Обычный способ отправки журналов в два места - просто использовать корневой журнал с двумя обработчиками, каждый со своим собственным форматированием.

Кроме того, даже если вам нужны два регистратора, вам не нужны отдельные карты console_logging_level_switch и file_logging_level_switch; вызов Logger.debug(msg) - это то же самое, что и вызов Logger.log(DEBUG, msg). Вам все равно нужно каким-то образом сопоставить имена пользовательских уровней debug и т.д. С стандартными именами debug и т.д., Но вы можете просто выполнить один поиск, вместо того, чтобы делать это один раз для каждого регистратора (плюс, если ваш имена - это просто стандартные имена с разными актами, вы можете обманывать).

Все это очень хорошо описано в разделе Несколько обработчиков и форматировщиков и остальной кулинарной книге для ведения журнала.

Единственная проблема со стандартным способом сделать это состоит в том, что вы не можете легко отключить ведение журнала консоли на основе сообщений за сообщением. Это потому, что это не нормальная вещь. Обычно вы просто регистрируетесь по уровням и устанавливаете уровень журнала выше в журнале файлов.

Но, если вы хотите большего контроля, вы можете использовать фильтры. Например, дайте FileHandler фильтру, который принимает все, и ваш ConsoleHandler фильтр, который требует чего-то, начинающегося с console, затем используйте фильтр 'console' if print_screen else ''. Это уменьшает WriteLog до почти однострочного.

Вам все еще нужны дополнительные две строки для удаления новых строк, но вы можете даже сделать это в фильтре или через адаптер, если хотите. (Опять же, см. Поваренную книгу.) И тогда WriteLog действительно является однострочным.

Ответ 2

Запись в Python безопасна в потоковом режиме:

Таким образом, у вас нет проблем в коде Python (library).

Процедура, которую вы вызываете из нескольких потоков (WriteLog), не записывается в какое-либо общее состояние. Таким образом, у вас нет проблем с вашим кодом.

Итак, вы в порядке.