Що таке "локальне зберігання потоків" в Python, і для чого він потрібен?


100

Як конкретно в Python, як поділяються змінні між потоками?

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

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

Спасибі заздалегідь!


2
Назва не відповідає питанню. Питання стосується спільного використання змінних між потоками, заголовок передбачає, що мова йде саме про локальне зберігання потоку
Casebash

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

Відповіді:


83

У 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 (); 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 (); 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 полягає в тому, що блокування Global Interpreter Lock означає, що лише один потік може одночасно виконувати код Python. Це не сильно допомагає, коли мова йде про синхронізацію доступу, оскільки, як правило, всі звичні проблеми з випередженням все ще застосовуються, і вам доведеться використовувати примітивні нитки, як і в інших мовах. Це означає, що вам потрібно переглянути, якщо ви використовували теми для продуктивності.


0

Я можу помилитися тут. Якщо ви знаєте інше, будь ласка, викладіть, оскільки це допоможе пояснити, чому потрібно використовувати нитку local ().

Це твердження, здається, не помиляється: "Якщо ви хочете атомно модифікувати все, до чого має інший потік, вам потрібно захистити його замком". Я думаю, що це твердження -> ефективно <- правильне, але не зовсім точне. Я думав, що термін "атомний" означає, що інтерпретатор Python створив фрагмент байтового коду, який не залишив місця для сигналу переривання в процесорі.

Я думав, що атомні операції - це шматки байтового коду Python, які не дають доступу до переривань. Заяви Python на зразок "running = True" є атомними. У цьому випадку вам не потрібно блокувати ЦП від перебоїв (я вважаю). Розбивка байтового коду Python захищена від переривання потоку.

Код Python типу "thread_running [5] = True" не є атомним. Тут є два фрагменти байтового коду Python; один для де-посилання на список () для об'єкта, а інший фрагмент байтового коду для присвоєння значенню об'єкту, у цьому випадку - "місце" у списку. Переривання може бути піднято -> між <- двома байт-кодами -> шматками <-. Тобто були погані речі.

Як нитка local () співвідноситься з "атомною"? Ось чому твердження здається мені неправильним. Якщо ви не можете пояснити?


1
Це виглядає як відповідь, але я вважаю проблематичним, я вважаю, що через запитання. Я б не просив пояснень у відповідь. Це те, що стосується коментарів.
Дхарман
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.