Что такое «локальное хранилище потоков» в Python и зачем оно мне нужно?

100

В частности, в Python, как переменные распределяются между потоками?

Хотя я использовал threading.Threadраньше, я никогда по-настоящему не понимал и не видел примеров того, как разделяются переменные. Распространены ли они между основным потоком и детьми или только между детьми? Когда мне нужно будет использовать локальное хранилище потоков, чтобы избежать этого совместного использования?

Я видел много предупреждений о синхронизации доступа к общим данным между потоками с помощью блокировок, но мне еще предстоит увидеть действительно хороший пример проблемы.

Заранее спасибо!

Майк
источник
2
Заголовок не соответствует вопросу. Вопрос в том, чтобы делиться переменными между потоками, название подразумевает, что речь идет именно о локальном хранилище потока
Casebash
2
@Casebash: судя по звучанию этого вопроса, Майк прочитал, что TLS необходим, чтобы избежать проблем, вызванных общими данными, но было неясно, какие данные были переданы по умолчанию, для чего они были переданы и как они стали общими. Я скорректировал заголовок, чтобы он лучше соответствовал вопросу.
Shog9,

Ответы:

84

В Python все является общим, за исключением локальных переменных функции (поскольку каждый вызов функции получает свой собственный набор локальных переменных, а потоки всегда являются отдельными вызовами функций). И даже тогда только сами переменные (имена, которые относятся к объектам) являются локальными для функции; сами объекты всегда глобальны, и на них может ссылаться что угодно. В этом отношении Threadобъект для конкретного потока не является особым объектом. Если вы храните Threadобъект где-нибудь, к которому имеют доступ все потоки (например, глобальная переменная), тогда все потоки могут получить доступ к этому одному Threadобъекту. Если вы хотите атомарно изменить что-либо , к чему другой поток имеет доступ, вы должны защитить это блокировкой. И все потоки, конечно же, должны использовать одну и ту же блокировку, иначе это будет не очень эффективно.

Если вам нужно фактическое локальное хранилище потока, вот тут-то и threading.localпригодится. Атрибуты threading.localне используются совместно между потоками; каждый поток видит только те атрибуты, которые он там разместил. Если вам интересно узнать о его реализации, источник находится в _threading_local.py в стандартной библиотеке.

Томас Воутерс
источник
1
Не могли бы вы подробнее рассказать о следующем предложении? «Если вы хотите атомарно изменить что-либо, что вы не только что создали в этом самом потоке, и не храните нигде, куда другой поток может попасть, вы должны защитить это блокировкой».
changyuheng
@changyuheng: Вот объяснение того, что такое атомные действия: cs.nott.ac.uk/~psznza/G52CON/lecture4.pdf
Том Басби
1
@TomBusby: Если никакие другие потоки не могут добраться до него, зачем нам защищать его блокировкой, т.е. зачем нам делать процесс атомарным?
changyuheng
2
Приведите, пожалуйста, быстрый пример: «сами объекты всегда глобальны, и все может ссылаться на них». Под ссылкой вы подразумеваете чтение, а не назначение / добавление?
переменная
@variable: я думаю, он имеет в виду, что значения не имеют области действия
user1071847
75

Рассмотрим следующий код:

#/usr/bin/env python

from time import sleep
from random import random
from threading import Thread, local

data = local()

def bar():
    print("I'm called from", data.v)

def foo():
    bar()

class T(Thread):
    def run(self):
        sleep(random())
        data.v = self.getName()   # Thread-1 and Thread-2 accordingly
        sleep(1)
        foo()
>> T (). Start (); Т (). Начало ()
Меня зовут из потока-2
Меня зовут из потока-1 

Здесь threading.local () используется как быстрый и грязный способ передать некоторые данные из run () в bar () без изменения интерфейса foo ().

Обратите внимание, что использование глобальных переменных не поможет:

#/usr/bin/env python

from time import sleep
from random import random
from threading import Thread

def bar():
    global v
    print("I'm called from", v)

def foo():
    bar()

class T(Thread):
    def run(self):
        global v
        sleep(random())
        v = self.getName()   # Thread-1 and Thread-2 accordingly
        sleep(1)
        foo()
>> T (). Start (); Т (). Начало ()
Меня зовут из потока-2
Меня зовут из потока-2 

Между тем, если бы вы могли позволить себе передавать эти данные в качестве аргумента foo () - это был бы более элегантный и хорошо продуманный способ:

from threading import Thread

def bar(v):
    print("I'm called from", v)

def foo(v):
    bar(v)

class T(Thread):
    def run(self):
        foo(self.getName())

Но это не всегда возможно при использовании стороннего или плохо спроектированного кода.

Ахачкинс
источник
18

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

>>> tls = threading.local()
>>> tls.x = 4 
>>> tls.x
4

Данные, хранящиеся в tls, будут уникальными для каждого потока, что поможет предотвратить непреднамеренное совместное использование.

Аарон Маенпаа
источник
2

Как и в любом другом языке, каждый поток в Python имеет доступ к одним и тем же переменным. Нет различия между «основным потоком» и дочерними потоками.

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

Ник Джонсон
источник
0

Я могу ошибаться здесь. Если вы знаете иное, поясните, что это поможет объяснить, почему нужно использовать поток local ().

Это утверждение кажется неправильным, а не неправильным: «Если вы хотите атомарно изменить что-либо, к чему другой поток имеет доступ, вы должны защитить это блокировкой». Я думаю, что это утверждение -> эффективно <- верно, но не совсем точно. Я думал, что термин «атомарный» означает, что интерпретатор Python создает фрагмент байт-кода, который не оставляет места для сигнала прерывания для процессора.

Я думал, что атомарные операции - это куски байтового кода Python, которые не дают доступа к прерываниям. Такие операторы Python, как «running = True», атомарны. В этом случае вам не нужно блокировать процессор от прерываний (я считаю). Разбивка байтового кода Python защищена от прерывания потока.

Код Python, такой как «thread_running [5] = True», не является атомарным. Здесь есть два фрагмента байтового кода Python; один для отмены ссылки на list () для объекта и другой фрагмент байтового кода для присвоения значения объекту, в данном случае «место» в списке. Прерывание может быть вызвано -> между <- двумя байтовыми кодами -> фрагментами <-. Это были плохие вещи.

Как thread local () соотносится с "атомарным"? Вот почему это утверждение мне кажется неверным. Если нет, вы можете объяснить?

DevPlayer
источник
1
Это похоже на ответ, но я полагаю, что это было проблематично из-за заданных вопросов. Я бы не стал просить пояснений в ответ. Вот для чего нужны комментарии.
Дхарман