Шаг 223.
Python: сборник рецептов.
Конкурентность. Хранение специфичного состояния потока

    На этом шаге мы рассмотрим способ решения этой задачи.

Задача

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

Решение

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

    Интересный практический пример использования локального для потока хранилища - менеджер контекста класса LazyConnection, который мы определяли на 132 шаге. Вот его слегка модифицированная версия, которая безопасно работает со многими потоками:

from socket import socket, AF_INET, SOCK_STREAM
import threading


class LazyConnection:
    def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
        self.address = address
        self.family = AF_INET
        self.type = SOCK_STREAM
        self.local = threading.local()

    def __enter__(self):
        if hasattr(self.local, 'sock'):
            raise RuntimeError('Already connected')
        self.local.sock = socket(self.family, self.type)
        self.local.sock.connect(self.address)
        return self.local.sock

    def __exit__(self, exc_ty, exc_val, tb):
        self.local.sock.close()
        del self.local.sock

    Здесь внимательно понаблюдайте, как используется атрибут self.local. Он инициализируется как экземпляр threading.local(). Другие методы затем манипулируют сокетом, который хранится как self.local.sock. Этого достаточно, чтобы сделать возможным использование экземпляра LazyConnection в нескольких потоках. Например:

def test(conn):
    from functools import partial

    # Connection closed
    with conn as s:
        # conn.__enter__() executes: connection open
        s.send(b'GET /index.html HTTP/1.0\r\n')
        s.send(b'Host: www.python.org\r\n')
        s.send(b'\r\n')
        resp = b''.join(iter(partial(s.recv, 8192), b''))
        # conn.__exit__() executes: connection closed

    print('Got {} bytes'.format(len(resp)))


if __name__ == '__main__':
    conn = LazyConnection(('www.python.org', 80))

    t1 = threading.Thread(target=test, args=(conn,))
    t2 = threading.Thread(target=test, args=(conn,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
Архив с этим файлом можно взять здесь.

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

Обсуждение

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

    В этом рецепте применение threading.local() заставляет класс LazyConnection поддерживать одно соединение на поток, что противоположно одному соединению на весь процесс. Это тонкое, но интересное отличие.

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

    На следующем шаге мы рассмотрим создание пула потоков.




Предыдущий шаг Содержание Следующий шаг