使用Python线程锁和循环导入时出现意外行为

5

我写了一个使用线程锁的简单测试程序。这个程序的行为与预期不符,而Python解释器并没有报错。

test1.py:

from __future__ import with_statement
from threading import Thread, RLock
import time
import test2

lock = RLock()

class Test1(object):
    def __init__(self):
        print("Start Test1")
        self.test2 = test2.Test2()
        self.__Thread = Thread(target=self.myThread, name="thread")
        self.__Thread.daemon = True
        self.__Thread.start()
        self.test1Method()

    def test1Method(self):
        print("start test1Method")
        with lock:
            print("entered test1Method")
            time.sleep(5)
            print("end test1Method")

    def myThread(self):
        self.test2.test2Method()

if __name__ == "__main__":
    client = Test1()
    raw_input()

test2.py:

from __future__ import with_statement
import time
import test1

lock = test1.lock

class Test2(object):
    def __init__(self):
        print("Start Test2")

    def test2Method(self):
        print("start test2Method")
        with lock:
            print("entered test2Method")
            time.sleep(5)
            print("end test2Method")

两个sleep同时执行了!这不是我使用锁时预期的结果。

当test2Method移动到test1.py中时,一切都正常。当我在test2.py中创建锁并在test1.py中导入它时,一切都正常。当我在一个单独的源文件中创建锁并同时在test1.py和test2.py中导入它时,一切都正常。

可能与循环导入有关。

但为什么Python没有对此进行抱怨呢?

3个回答

3
在Python中,当你使用$ python test1.py执行python脚本时,你的test1.py将被导入为__main__而不是test1,所以如果你想获取启动脚本中定义的锁,你不应该使用import test1,而应该使用import __main__,因为如果你使用第一种方法,你会创建另一个锁,这个锁与__main__.lock不同 (test1.lock != __main__.lock)。

所以对于你的问题,有一种修复方法(远非最佳),并且可以看到发生了什么,你可以将两个脚本更改为如下:

test1.py:

from __future__ import with_statement
from threading import Thread, RLock
import time

lock = RLock()

class Test1(object):
    def __init__(self):
        print("Start Test1")
        import test2    # <<<<<<<<<<<<<<<<<<<<<<<< Import is done here to be able to refer to __main__.lock.
        self.test2 = test2.Test2()
        self.__Thread = Thread(target=self.myThread, name="thread")
        self.__Thread.daemon = True
        self.__Thread.start()
        self.test1Method()

    def test1Method(self):
        print("start test1Method")
        with lock:
            print("entered test1Method")
            time.sleep(5)
            print("end test1Method")

    def myThread(self):
        self.test2.test2Method()

if __name__ == "__main__":
    client = Test1()
    raw_input()

test2.py:

from __future__ import with_statement
import time
# <<<<<<<<<<<<<<<<<<<<<<<<<<<<< test1 is changed to __main__ to get the same lock as the one used in the launched script.
import __main__

lock = __main__.lock

class Test2(object):
    def __init__(self):
        print("Start Test2")

    def test2Method(self):
        print("start test2Method")
        with lock:
            print("entered test2Method")
            time.sleep(5)
            print("end test2Method")

HTH,


谢谢您的解释。Python对我来说是相当新的,我从未遇到过这种行为。我很高兴我问了,因为它在其他非多线程的情况下也可能会发生。 - user1997293
@user1997293:是的,你说得对,这种行为非常普遍,很高兴我的答案有帮助:) - mouad

1

import语句之前和之后使用print语句,并在创建id(lock)后立即打印,可以发现实际上创建了两个锁。似乎该模块被导入了两次,而mouad在他的答案中解释说这是因为test1.py首先作为__main__导入,然后作为test1导入,这导致锁被实例化两次。

尽管如此,使用全局锁也不是一个好的解决方案。有几种更好的解决方案,我认为你会找到其中一种适合你的需求。

  • 将锁实例化为Test1的类变量,并将其作为参数传递给Test2

  • __init__中将锁实例化为Test1的普通变量,并将其作为参数传递给Test2

  • if __name__ == "__main__"块中实例化锁并将其传递给Test1,然后从Test1传递给Test2

  • if __name__ == "__main__"块中实例化锁,并首先使用锁实例化Test2,然后将Test2实例和锁一起传递给Test1。(这是最解耦的方法,我建议采用这种方法。至少它会简化单元测试。)

以下是最后一种方法的代码:

test1.py:

class Test1(object):
    def __init__(self, lock, test2):
        print("Start Test1")
        self.lock = lock
        self.test2 = test2
        self.__Thread = Thread(target=self.myThread, name="thread")
        self.__Thread.daemon = True
        self.__Thread.start()
        self.test1Method()

    def test1Method(self):
        print("start test1Method")
        with self.lock:
            print("entered test1Method")
            time.sleep(1)
            print("end test1Method")

    def myThread(self):
        self.test2.test2Method()

if __name__ == "__main__":
    lock = RLock()
    test2 = test2.Test2(lock)
    client = Test1(lock, test2)

test2.py:

class Test2(object):
    def __init__(self, lock):
        self.lock = lock
        print("Start Test2")

    def test2Method(self):
        print("start test2Method")
        with self.lock:
            print("entered test2Method")
            time.sleep(1)
            print("end test2Method")

0

正如其他人所说,问题不在于线程,而是在于您特殊的循环导入情况。

为什么是特殊的呢?因为通常的工作流程(mod1导入mod2mod2导入mod1)看起来像这样:

  1. 你想使用模块 mod1,你需要导入它(import mod1)。

  2. 当 Python 找到它时,解释器将其添加到 sys.modules 并开始执行代码。

  3. 当它到达带有 import mod2 的行时,它停止执行 mod1 并开始执行 mod2

  4. 当解释器在 mod2 中到达 import mod1 时,它不会加载 mod1,因为它已经被添加到了 sys.modules

  5. 之后(除非 mod2 中的某些代码访问来自 mod1 的未初始化资源),解释器完成执行 mod2mod1

但在您的情况下,在第4步中,Python会再次执行test1,因为sys.modules中没有test1!原因是您一开始没有导入它,而是从命令行运行它。

所以,不要使用循环导入-正如您所看到的,这真的很混乱。


事实上,我通常会避免循环导入。这次只是一个小的测试程序,最初旨在查看锁是否像单例一样运行,但它并没有。这个第一次测试没有循环导入。然后我稍微改了一下程序,使其能够工作,但是不幸的是出现了循环导入。 - user1997293

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接