过滤器是否是线程安全的

时间:2015-06-03 12:15:29

标签: python

我有一个更新名为l的列表的线程。我是否正确地说从另一个线程执行以下操作是线程安全的?

filter(lambda x: x[0] == "in", l)

如果它不是线程安全的,那么这是正确的方法:

import threading
import time
import Queue

class Logger(threading.Thread):
    def __init__(self, log):
        super(Logger, self).__init__()
        self.log = log
        self.data = []
        self.finished = False
        self.data_lock = threading.Lock()

    def run(self):
        while not self.finished:
            try:
                with self.data_lock: 
                    self.data.append(self.log.get(block=True, timeout=0.1))
            except Queue.Empty:
                pass

    def get_data(self, cond):
        with self.data_lock: 
            d = filter(cond, self.data)      
        return d 

    def stop(self):
        self.finished = True
        self.join()  
        print("Logger stopped")

其中get_data(self, cond)方法用于以线程安全的方式检索self.data中的一小部分数据。

2 个答案:

答案 0 :(得分:7)

首先,在标题中回答您的问题:filter只是一个功能。因此,它的线程安全性将依赖于您使用它的数据结构。

正如评论中已经指出的那样,列表操作本身在CPython中是线程安全的并且受GIL保护,但这可能只是CPython的一个实现细节,你不应该真正依赖它。即使你可以依赖它,它们的一些操作的线程安全可能并不意味着你所说的线程安全:

问题是用filter迭代序列通常不是原子操作。可以在迭代期间更改序列。根据迭代器底层的数据结构,这可能会产生或多或少的奇怪效果。克服此问题的一种方法是迭代使用一个原子动作创建的序列的副本。对tupleliststring等标准序列执行此操作的最简单方法是使用切片运算符,如下所示:

filter(lambda x: x[0] == "in", l[:])

除了这对于其他数据类型不一定是线程安全的,但是这有一个问题:它只是一个浅拷贝。由于列表的元素似乎也是类似列表,另一个线程可以并行执行del l[1000][:]以清空其中一个内部列表(也在浅层副本中指向)。这会使您的过滤器表达式失败并显示IndexError

所有这一切,使用锁来保护对列表的访问并不是一种耻辱,我绝对推荐它。根据数据的更改方式以及如何处理返回的数据,在保持锁定时深度复制元素并返回这些副本甚至是明智之举。这样,您可以保证一旦返回,过滤条件就不会突然改变返回的元素。

WRT。您的Logger代码:我不能100%确定您打算如何使用此代码,以及在一个队列中运行多个线程并join它们是否至关重要。对我来说看起来很奇怪的是你永远不会使用Queue.task_done()(假设它的self.logQueue)。您对队列的轮询也可能是浪费。如果您不需要线索的join,我建议至少转换锁定获取:

class Logger(threading.Thread):
    def __init__(self, log):
        super(Logger, self).__init__()
        self.daemon = True
        self.log = log
        self.data = []
        self.data_lock = threading.Lock()

    def run(self):
        while True:
            l = self.log.get()  # thread will sleep here indefinitely
            with self.data_lock: 
                self.data.append(l)
            self.log.task_done()

    def get_data(self, cond):
        with self.data_lock: 
            d = filter(cond, self.data)
            # maybe deepcopy d here
        return d

在外部,您仍然可以执行log.join()以确保处理log队列的所有元素。

答案 1 :(得分:4)

如果一个线程写入列表而另一个线程读取该列表,则必须同步这两个线程。读者使用filter(),索引或迭代,或者作者是使用append()还是使用任何其他方法,这与方面无关。

在您的代码中,您使用threading.Lock实现必要的同步。由于您只访问with self.data_lock上下文中的列表,因此访问是互斥的。

总之,您的代码在线程之间的列表处理方面是正式的。但是:

  • 您可以在没有锁定的情况下访问self.finished,这是有问题的。分配给该成员将更改self,即将对象映射到相应成员,因此应该同步。实际上,这不会有害,因为TrueFalse是全局常量,在最坏的情况下,在一个线程中设置状态和在另一个线程中查看状态之间会有短暂的延迟。它仍然很糟糕,因为它正在形成习惯。
  • 通常,当您使用锁定时,始终文档会锁定此锁定的对象。另外,记录哪个对象被哪个线程访问。 self.finished共享并需要同步这一事实显而易见。此外,在公共函数和数据与私有函数之间进行视觉区分(以_underscore开头,请参阅PEP 8)有助于跟踪此情况。它也有助于其他读者。
  • 类似的问题是你的基类。一般来说,继承threading.Thread是一个坏主意。而是包含一个线程类的实例,并给它一个像self._main_loop这样的函数来运行。原因是你说你的Logger是一个Thread并且它的所有基类'公共成员也是你班级的公共成员,这可能是一个比你想要的更广泛的界面。 / LI>
  • 你永远不应该锁定锁。在您的代码中,您可以使用互斥锁锁定self.log.get(block=True, timeout=0.1)。在那个时候,即使没有实际发生任何事情,也没有其他线程可以调用并完成对get_data()的调用。实际上只有一个小窗口可以解锁互斥锁并再次锁定它,而get_data()的调用者必须等待,这对性能非常不利。我甚至可以想象你的问题是由这导致的非常糟糕的表现所激发的。相反,请在没有锁定的情况下调用log.get(..),它不应该需要一个。然后,在锁定的情况下,将数据附加到self.data并检查self.finished