Когда использовать локальную память потоков в Python?

#python #multithreading #python-multithreading

#python #многопоточность #python-многопоточность

Вопрос:

Я только начинаю с Python и наткнулся на локальную память потоков. Я написал небольшую программу, которая использует потоки:

 #!/usr/bin/env python3

import logging
import signal
import threading
import time

class WorkerThread(threading.Thread):
    def __init__(self, idx):
        threading.Thread.__init__(self)
        self.thread_index = idx
        self.thread_alive = True

    def run(self):
        logging.info(f'Thread {self.thread_index} is starting up!')

        while self.thread_alive:
            logging.info(f'Thread {self.thread_index} is still running.')
            time.sleep(1)

        logging.info(f'Thread {self.thread_index} is stopping!')

    def kill(self):
        self.thread_alive = False

def main():
    logging.basicConfig(format = '%(levelname)s: %(message)s', level = logging.INFO)

    def signal_handler(sig, frame):
        logging.info('Ctrl c pressed, killing threads and shutting down ...')
        nonlocal threads
        for thread in threads:
            thread.kill()

    signal.signal(signal.SIGINT, signal_handler)

    logging.info('Signal handler registered, starting threads ...')

    threads = []
    for i in range(0, 3):
        thread = WorkerThread(i)
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()

    signal.signal(signal.SIGINT, signal.SIG_DFL)

if __name__ == '__main__':
    main()
  

Эта программа работает должным образом и выводит что-то вроде:

 > python3 main.py
INFO: Signal handler registered, starting threads ...
INFO: Thread 0 is starting up!
INFO: Thread 0 is still running.
INFO: Thread 1 is starting up!
INFO: Thread 2 is starting up!
INFO: Thread 1 is still running.
INFO: Thread 2 is still running.
INFO: Thread 0 is still running.
INFO: Thread 1 is still running.
INFO: Thread 2 is still running.
INFO: Thread 0 is still running.
INFO: Thread 2 is still running.
INFO: Thread 1 is still running.
INFO: Thread 2 is still running.
INFO: Thread 1 is still running.
INFO: Thread 0 is still running.
INFO: Thread 1 is still running.
INFO: Thread 2 is still running.
INFO: Thread 0 is still running.
^CINFO: Ctrl c pressed, killing threads and shutting down ...
INFO: Thread 2 is stopping!
INFO: Thread 1 is stopping!
INFO: Thread 0 is stopping!
  

В этом случае thread_index thread_alive переменные and специфичны для каждого потока, поскольку они специфичны для каждого объекта. Но есть также threading.local() функция, которая создает локальную память потока. Итак, я попытался использовать это, поскольку я хочу, чтобы мои переменные зависели от потока. Я использовал его после определения класса:

 # imports and shebang

class WorkerThread(threading.Thread):
    thread_index = threading.local()
    thread_alive = threading.local()

# everything else stays the same
  

Но использование этого ничего не меняет, результат остается прежним. Итак, мои вопросы:

  • является ли эта локальная память потока для другого варианта использования или первая программа работала только случайно?
  • для чего нужны варианты использования threading.local() , поскольку создание объектно-ориентированных (нестатических) переменных, похоже, тоже работает?

Ответ №1:

threading.local() предназначен для случаев, когда вы не можете или не хотите изменять классы, реализующие потоки.

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

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

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

В этом случае давайте представим, что вы хотите сохранить некоторый контекст для обрабатываемого запроса (например, для текущего пользователя, вошедшего в систему), чтобы вы могли получить к нему доступ во время обработки запроса, но не нужно явно передавать его в каждой функции. Вы не можете добавить эту currentUser переменную в класс потоков, поскольку у вас нет контроля над ней. Но вы можете использовать threading.local() для ее хранения. И запросы, которые одновременно обрабатываются в нескольких потоках, будут иметь свои собственные копии этого.

То же самое применимо для ваших собственных творений. Когда программа становится более сложной, и вам нужно отделить код инфраструктуры (управление потоками) от логики вашего приложения, может случиться так, что вы не захотите добавлять переменные, специфичные для потоков, в классы потоков и использовать threading.local() вместо этого.

Ответ №2:

Личное мнение: я бы никогда не использовал локальное хранилище потоков в новом коде.

IMO, лучшее использование для локального хранилища потоков — это когда у вас есть старый модуль, который не зависит от потоков, и он использует глобальные переменные, и вы хотите иметь возможность вызывать старый модуль из нескольких потоков в какой-то новой программе. В этом случае простая замена всех глобальных переменных в старом модуле на локальные потоки может быть дешевым способом позволить каждому потоку в новой программе эффективно иметь свою собственную «копию» модуля.

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