【问题标题】:how to make a thread-safe global counter in python如何在python中制作一个线程安全的全局计数器
【发布时间】:2016-01-29 15:29:58
【问题描述】:

我正在创建一个threading.Timer(2,work) 运行线程。在每个工作函数内部,在某些情况下,全局计数器必须递增,而不会在派生的工作线程之间访问计数器变量发生冲突。

我试过Queue.Queue 分配的计数器以及threading.Lock()。这是实现线程安全的全局增量变量的最佳方式。

之前有人在这里提问:Python threading. How do I lock a thread?

【问题讨论】:

    标签: python multithreading


    【解决方案1】:

    不确定您是否已经尝试过这种特定的语法,但对我来说这一直很有效:

    定义一个全局锁:

    import threading
    threadLock = threading.Lock()
    

    然后你必须在每次增加单个线程中的计数器时获取和释放锁:

    with threadLock:
        global_counter += 1
    

    【讨论】:

    • 在 Python 3 中,您可以使用Lock(和RLock,以及任何threading 类实例)作为with 上下文。代码更具可读性。
    • @Arĥimedeςℳontegasppαℭacilhας 同样在 Python2 中,它使其异常安全,所以我对其进行了编辑。
    【解决方案2】:

    一种解决方案是使用multiprocessing.Lock 保护计数器。你可以把它放在一个类中,像这样:

    from multiprocessing import Process, RawValue, Lock
    import time
    
    class Counter(object):
        def __init__(self, value=0):
            # RawValue because we don't need it to create a Lock:
            self.val = RawValue('i', value)
            self.lock = Lock()
    
        def increment(self):
            with self.lock:
                self.val.value += 1
    
        def value(self):
            with self.lock:
                return self.val.value
    
    def inc(counter):
        for i in range(1000):
            counter.increment()
    
    if __name__ == '__main__':
        thread_safe_counter = Counter(0)
        procs = [Process(target=inc, args=(thread_safe_counter,)) for i in range(100)]
    
        for p in procs: p.start()
        for p in procs: p.join()
    
        print (thread_safe_counter.value())
    

    上面的 sn-p 最初取自 Eli Bendersky 的博客,here

    【讨论】:

    • 你需要这样做吗。如果你准备好使用 CPython,那么 Global Interpreter Lock 意味着每个 python 操作都是原子的,所以self.value += 1 应该是原子的。还是我误解了什么?
    • 在 CPython 中使用线程大多是荒谬的,而且问题不是 CPython 特定的。即使在 CPython 中,有些事情也会发生在 GIL 之外,所以我宁愿不鼓励坏习惯。
    • 也适用于以后偶然发现此问题的任何人...self.value += 1 不是相关意义上的单个 python 操作,因为它编译成多个字节码。实际上是tmp1 = self.value; tmp2 = tmp1.__iadd__(1); self.value = tmp2,这三个语句中的每一个都是原子的,但整个序列绝对不是。
    • @MichaelFoukarakis 线程绝对不是浪费时间。使用线程可以比不使用线程更快地下载 1000 个网站。
    • @MichaelFoukarakis 线程有其用途。例如,在制作非阻塞单核程序(如 GUI)时。线程不一定与多处理有关。新线程不要求它在另一个核心上运行。这是对线程的误解,很多人似乎都有。显然,它们大多不是荒谬的。对于像线程这样重要的东西,这是一个过于宽泛的陈述。
    【解决方案3】:

    如果您使用的是 CPython1,则无需显式锁定即可:

    import itertools
    
    class Counter:
        def __init__(self):
            self._incs = itertools.count()
            self._accesses = itertools.count()
    
        def increment(self):
            next(self._incs)
    
        def value(self):
            return next(self._incs) - next(self._accesses)
    
    my_global_counter = Counter()
    

    我们需要两个计数器:一个用于计数增量,一个用于计数value() 的访问。这是因为itertools.count 没有提供访问 current 值的方法,只有 next 值。所以我们需要通过请求值来“撤消”我们产生的增量。

    这是线程安全的,因为 itertools.count.__next__() 在 CPython 中是原子的(感谢 GIL!),我们不会保留差异。

    请注意,如果value() 被并行访问,确切的数字可能不会完全稳定。它可以是正比于访问的线程数的余量。理论上,self._incs 可以在一个线程中首先更新,而self._accesses 在另一个线程中首先更新。但总体而言,系统不会因未保护的写入而丢失任何数据;它总是会稳定到正确的值。

    1 并非所有 Python 都是 CPython,但很多(大多数?)是。

    2 感谢https://julien.danjou.info/atomic-lock-free-counters-in-python/ 最初的想法是使用itertools.count 来增加和更正第二个访问计数器。他们没把所有的锁都拆掉就停了下来。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-11-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-23
      • 2015-07-05
      • 1970-01-01
      相关资源
      最近更新 更多