设置要在线程否则完成时执行的行为

时间:2019-07-09 19:55:38

标签: python multithreading thread-safety

我的模块中具有两个功能:do_something()change_behavior()

函数do_something()在默认情况下执行 A 。调用change_behavior()之后,do_something()会执行 B事

我希望此过渡特定于线程。也就是说,任何新线程在调用do_something()时都会发生事物A ,但是如果该线程调用change_behavior(),则将发生事物B 而是当它继续调用do_something()时。

每个线程应该是独立的,以便一个调用change_behavior()的线程不会影响do_something()对于其他线程的行为。


我本能的解决方案是将行为绑定到线程的ID(通过threading.get_ident()进行评估)。函数do_something()检查本地表中是否有线程的ID,并相应地调整其行为。同时,函数change_behavior()仅将当前线程添加到该注册表中。这在任何给定时间都是可行的,因为永远不会有两个具有相同ID的并发线程。

当当前线程集加入并经过时间,并且父线程使一大堆线程更多时,就会出现问题。新线程之一具有与先前线程之一相同的ID,因为有时会重复使用线程ID。该线程调用do_something(),因为它已经在注册表中,所以它执行事物B 而不是事物A

要解决此问题,我需要从注册表中删除线程ID,介于具有该ID的第一个线程何时结束与具有该ID的第二个线程开始之间。我想出了一些假想的想法:

  • 定期检查每个线程ID是否仍处于活动状态。这是有问题的,因为它既浪费CPU资源,又可能在线程被破坏并在两次更新之间重新创建时丢失线程
  • 附加一个方法钩子,以在线程加入时被调用。除了下一个想法,我不确定如何做到这一点
  • 作为change_behavior()的一部分,劫持/替换当前线程的._quit()方法,该方法首先从注册表中删除线程的ID。这似乎是一种不好的做法,并且可能会中断。

用例的另一个方面是,如果可能的话,我希望新线程继承其父线程的当前行为,以便用户不必手动设置他们创建的每个标志-但这是与如何存储有关胎面状态的信息相比,与线程完成时的信息更为相关,这使得它与该特定问题的相关性略微降低。

我正在寻找有关这些特定解决方案中的任何一个是理想的,标准的还是惯用的,以及在此用例中是否有想要做的事情的指南。


@TarunLalwani在评论中建议使用threading.local()。我已经对其进行了调查,但它很有用,但它不能解决我要处理的其他用例-当父线程创建新的子线程时,我希望它们继承父线程的状态。我当时正在考虑通过替换Thread.__init__()来实现此目的,但是使用local()通常与该用例不兼容,因为我无法将变量从父线程传递给子线程。


我也一直在进行尝试,只是将属性保存到线程本身,因此更加成功:

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

this 的问题在于,出于使我完全困惑的原因,模块命名空间中当前值为current_thread.my_reference的任何其他变量也被设置为{{ 1}} 。我不知道为什么,而且我一直无法在MVE中复制问题(尽管即使在重新启动后,它在我的IDE中也一直发生)。就像my other currently-active question所暗示的那样,我在此处设置的对象是对输出流的引用(我在该答案中描述的每个对中间IO流的实例的引用都已被该方法所调用的文件描述符替换。 ),如果这与它有任何关系,但是我无法想象为什么在这种情况下对象的类型会影响引用的工作方式。

1 个答案:

答案 0 :(得分:4)

我的答案是对您的问题的非常简单的答案,因此我想知道我是否错过了什么。基本上,我认为您应该避免将外部对象的当前状态存储在模块中。

您需要将状态(如果调用了change_behavior以及其他一些数据)存储在某个地方。您有两个主要选择:将状态存储在模块中或将状态存储在线程本身中。除了将状态存储在模块中存在的问题之外,人们还希望模块(主要)是无状态的,因此我认为您应该坚持使用后者并将数据存储在线程中。

版本1

如果将状态存储在字段中,则在创建的属性名称和现有属性名称之间会有少许冲突的风险,但是如果文档清晰且选择了好名字,则应该没问题。

没有setattrhasattr的简单概念证明(我没有检查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)>)

缺点是即使您不需要它也必须使用线程。