了解 Python 中的多线程和锁(概念和示例)

Understanding multi-threading and locks in Python (concept and example)

我为使用它的编程项目研究了多线程(这里是初学者...)。如果您认为我的以下陈述是正确的,或者对错误或需要更正的陈述发表评论,我将不胜感激。

  1. 锁是一个可以通过引用传递给函数、方法...的对象。然后(在本例中)函数可以使用该锁定对象引用来安全地操作数据(在本例中为变量)。它通过获取锁、修改变量然后释放锁来实现。
  2. 可以创建一个线程来定位一个函数,该函数可能会获得对锁的引用(然后实现上述内容)。
  3. 保护特定变量、对象等
  4. 锁不保护或做任何事情,除非它被获取(和释放)。
  5. 因此,程序员有责任使用锁以实现所需的保护。
  6. 如果在线程 A 执行的函数内部获取锁,这 对任何其他 运行 线程 B 没有直接影响。线程 A 和 B 引用了同一个锁对象。
  7. 仅当线程 B 的目标函数想要获取 相同的 锁(即通过相同的引用锁对象)时,该锁已被线程 A 的目标函数获取那时,锁会影响两个线程,因为线程 B 将暂停进一步执行,直到线程 A 所针对的函数再次释放锁。
  8. 因此,锁定的锁只会暂停线程的执行,如果它的目标函数想要(并等待)自己获取完全相同的锁。因此,线程A获取锁,只能阻止线程B获取同一个锁,仅此而已。

如果我想在设置变量时使用锁来防止竞争条件,我(作为程序员)需要:

  1. 将锁传递给所有 函数,这些函数的目标是要设置变量的线程和
  2. 在我设置变量之前每个函数和每次获取锁(然后释放它)。 (*)
  3. 如果我只创建一个针对函数的线程,而没有向它提供对锁定对象的引用并让它设置变量或
  4. 如果我通过目标函数具有锁定对象的线程设置变量,但在操作之前没有获取它,我将无法实现线程-变量的安全设置。

(*) 只要变量不能被其他线程访问,就应该获取锁。现在,我喜欢将其与数据库事务进行比较...我锁定数据库(~获取锁)直到我的指令集完成,然后我提交(~释放锁)。


示例如果我想创建一个class,其成员_value应该在一个线程中set -安全时尚,我会实施这两个版本之一:

    class Version1:
        def __init__(self):
            self._value:int = 0
            self._lock:threading.Lock = threading.Lock()
        
        def getValue(self) -> int:
            """Getting won't be protected in this example."""
            return self._value
        
        def setValue(self, val:int) -> None:
            """This will be made thread-safe by member lock."""
            with self._lock:
                self._value = val
            
    v1 = Version1()
    t1_1 = threading.Thread(target=v1.setValue, args=(1)).start()
    t1_2 = threading.Thread(target=v1.setValue, args=(2)).start()
    
    
    class Version2:
        def __init__(self):
            self._value:int = 0
        
        def getValue(self) -> int:
            """Getting won't be protected in this example."""
            return self._value
        
        def setValue(self, val:int, lock:threading.Lock) -> None:
            """This will be made thread-safe by injected lock."""
            with self._lock:
                self._value = val
            
    v2 = Version2()
    l = threading.Lock()
    t2_1 = threading.Thread(target=v2.setValue, args=(1, l)).start()
    t2_2 = threading.Thread(target=v2.setValue, args=(2, l)).start()
  1. Version1中,我作为class提供者,可以保证设置_value始终是线程安全的...
  2. ...因为在 Version2 中,我的 class 的用户可能会将不同的锁对象传递给两个生成的线程,从而使锁保护失效。
  3. 如果我想让 class 的用户自由地将 _value 的设置包含到应该以线程安全方式执行的更大的步骤集合中,我可以将 Lock 引用注入 Version1__init__ 函数并将其分配给 _lock 成员。因此,class 的线程安全操作将得到保证,同时仍然允许 class 的用户为此目的使用“她自己的”锁。

0-15 的分数现在将评估我对锁的(误解)理解程度...:-D

  1. 使用全局变量进行锁也很常见。这取决于锁保护的是什么。
  2. 是的,虽然有点无意义。任何函数都可以使用锁,而不仅仅是作为线程目标的函数。
  3. 如果您的意思是锁与其保护的数据之间没有直接 link,那是对的。但是您可以定义一个数据结构,其中包含需要保护的值和对其锁的引用。
  4. 没错。虽然如我在3中所说,你可以定义一个数据结构来封装数据和锁。您可以将其设为 class 并让 class 方法根据需要自动获取锁。
  5. 正确。但是请参阅 4 以了解如何自动执行此操作。
  6. 正确。
  7. 正确。
  8. 正确。
  9. 如果不是全局锁则更正。
  10. 部分正确。如果您只是 读取 变量,您还应该经常获取锁。如果读取对象不是原子的(例如,它是一个列表并且您正在读取多个元素,或者您读取同一个标量对象可变次数并期望它是稳定的),您需要防止另一个线程在您读取时修改它正在阅读。
  11. 正确。
  12. 正确。
  13. 正确。这是我在上面 3 和 4 中描述的示例。
  14. 正确。这就是为什么 13 中的设计通常更好。
  15. 这很棘手,因为锁定的粒度需要反映所有需要保护的对象。您的 class 仅保护该变量的赋值——它会在与 caller-provided 锁关联的所有其他步骤完成之前释放锁。