Установить поведение, которое будет выполняться, когда поток завершится - PullRequest
5 голосов
/ 09 июля 2019

Мой модуль имеет две функции: do_something() и change_behavior().

Функция do_something() выполняет Thing A по умолчанию.После вызова change_behavior() do_something() выполняет Thing B .

Я хочу, чтобы этот переход был специфичным для потока.То есть любой новый поток будет иметь Thing A , когда он вызывает do_something(), но если этот поток вызывает change_behavior(), тогда Thing B будет происходить, когда он продолжает вызыватьdo_something().

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


Мое инстинктивное решениеэто должно привязывать поведение к идентификатору потока (оценивается с помощью threading.get_ident()).Функция do_something() проверяет локальную таблицу на предмет наличия в ней идентификатора потока и соответствующим образом корректирует ее поведение.Между тем, функция change_behavior() просто добавляет текущий поток в этот реестр.Это работает в любой момент времени, потому что никогда не бывает двух параллельных потоков с одинаковым идентификатором.

Проблема возникает, когда текущий набор потоков присоединяется, и время проходит, и родительский поток делает на целую кучу больше потоков,Один из новых потоков имеет тот же идентификатор, что и один из предыдущих потоков, поскольку идентификаторы потоков иногда используются повторно.Этот поток вызывает do_something(), и поскольку он уже находится в реестре, он делает Thing B вместо Thing A .

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

  • Периодически проверяйте, активен ли каждый идентификатор потока.Это проблематично, так как он тратит впустую ресурсы ЦП и может пропустить, если поток уничтожен, а затем воссоздан между тиками
  • Прикрепите ловушку метода, вызываемую при каждом присоединении потока.Я не уверен, как это сделать, кроме следующей идеи
  • Как часть change_behavior(), перехватите / замените метод ._quit() текущего потока на тот, который сначала удаляет идентификатор потока из реестра.Это кажется плохой практикой и может привести к поломке.

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

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


Использование threading.local() было предложено в комментариях @TarunLalwani.Я исследовал это, и это полезно, но он не учитывает другой вариант использования, о котором я хотел бы позаботиться - когда родительский поток создает новые подпотоки, я хочу, чтобы они наследовали состояние родительского потока,Я думал о достижении этого путем замены Thread.__init__(), но использование local() было бы несовместимо с этим вариантом использования в целом, так как я не смог бы передавать переменные из родительских в дочерние потоки.


Я также более успешно экспериментировал с простым сохранением своих атрибутов в самих потоках:

current_thread = threading.current_thread()
setattr(current_thread, my_reference, new_value)

Проблема с этим заключается в том, что по причине, которая меня полностью озадачивает, любая другая переменная в пространстве имен модуля, чье значение в настоящее время current_thread.my_reference также устанавливается на new_value.Я понятия не имею, почему, и я не смог воспроизвести проблему в MVE (хотя это происходит последовательно в моей IDE, даже после перезапуска).Как показывает мой другой активный на данный момент вопрос , объекты, которые я задаю здесь, являются ссылками на выходные потоки (каждая ссылка на экземпляр промежуточной потоковой передачи ввода-вывода, который я описал в этом ответе, заменяется дескриптором файлас которым вызывается этот метод), если это имеет какое-либо отношение к нему, но я не могу себе представить, почему тип объекта будет влиять на работу ссылок в этом случае.

1 Ответ

4 голосов
/ 17 июля 2019

Мой ответ - очень простой ответ на ваш вопрос, поэтому мне интересно, пропустил ли я что-то. По сути, я думаю, что вам следует избегать хранения текущего состояния внешних объектов в вашем модуле.

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

Версия 1

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

Простое подтверждение концепции, без setattr или hasattr (я не проверял исходный код CPython, но, возможно, странное поведение исходит от setattr):

module1.py

import threading
import random
import time

_lock = threading.Lock()

def do_something():
    with _lock:
        t = threading.current_thread()
        try:
            if t._my_module_s:
                print(f"DoB ({t})")
            else:
                print(f"DoA ({t})")
        except AttributeError:
            t._my_module_s = 0
            print(f"DoA ({t})")

    time.sleep(random.random()*2)

def change_behavior():
    with _lock:
        t = threading.current_thread()
        print(f"Change behavior of: {t}")
        t._my_module_s = 1

test1.py

import random
import threading
from module1 import *

class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        n = random.randint(1, 10)
        for i in range(n):
            do_something()
        change_behavior()
        for i in range(10-n):
            do_something()

thread_1 = MyThread()
thread_2 = MyThread()
thread_1.start()
thread_2.start()
thread_1.join()
thread_2.join()

Выход 1

DoA (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
DoA (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-1, started 140155115792128)>)
Change behavior of: <MyThread(Thread-1, started 140155115792128)>
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoA (<MyThread(Thread-2, started 140155107399424)>)
Change behavior of: <MyThread(Thread-2, started 140155107399424)>
DoB (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoB (<MyThread(Thread-1, started 140155115792128)>)
DoB (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-2, started 140155107399424)>)
DoB (<MyThread(Thread-2, started 140155107399424)>)

Версия 2

Если вы уверены, что конечный пользователь будет использовать ваш модуль внутри потоков, вы можете предоставить ему / ей удобный способ сделать это. Идея состоит в том, чтобы обрабатывать темы самостоятельно Просто поместите пользовательскую функцию в поток и сохраните состояние потока в этом потоке, как описано выше. Разница в том, что вы являетесь владельцем дочернего класса Thread и избегаете риска конфликта имен. Плюс, на мой взгляд, код становится чище:

module2.py

import threading
import random
import time

_lock = threading.Lock()

def do_something():
    with _lock:
        t = threading.current_thread()
        t.do_something() # t must be a _UserFunctionWrapper
    time.sleep(random.random()*2)

def change_behavior():
    with _lock:
        t = threading.current_thread()
        t.change_behavior() # t must be a _UserFunctionWrapper

def wrap_in_thread(f):
    return _UserFunctionWrapper(f)

class _UserFunctionWrapper(threading.Thread):
    def __init__(self, user_function):
        threading.Thread.__init__(self)
        self._user_function = user_function
        self._s = 0

    def change_behavior(self):
        print(f"Change behavior of: {self}")
        self._s = 1

    def do_something(self):
        if self._s:
            print(f"DoB ({self})")
        else:
            print(f"DoA ({self})")

    def run(self):
        self._user_function()

test2.py

import random
from module2 import *

def user_function():
    n = random.randint(1, 10)
    for i in range(n):
        do_something() # won't work if the function is not wrapped
    change_behavior()
    for i in range(10-n):
        do_something()

thread_1 = wrap_in_thread(user_function)
thread_2 = wrap_in_thread(user_function)
thread_1.start()
thread_2.start()
thread_1.join()
thread_2.join()

Выход 2

DoA (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
Change behavior of: <_UserFunctionWrapper(Thread-1, started 140193896072960)>
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoA (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
Change behavior of: <_UserFunctionWrapper(Thread-2, started 140193887680256)>
DoB (<_UserFunctionWrapper(Thread-2, started 140193887680256)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)
DoB (<_UserFunctionWrapper(Thread-1, started 140193896072960)>)

Недостатком является то, что вы должны использовать поток, даже если он вам не нужен.

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