是itertools.tee()线程安全(Python)的结果

时间:2011-07-15 06:49:03

标签: python thread-safety tee

假设我有这个Python代码:

from itertools import count, tee
original = count()     # just an example, can be another iterable
a, b = tee(original)

问题是,如果我开始在一个线程中迭代“a”并且同时在另一个线程中迭代“b”,会有任何问题吗?显然,a和b共享一些数据(原始可迭代,+一些额外的东西,内部缓冲区或其他东西)。那么,a.next()和b.next()在访问这个共享数据时会进行适当的锁定吗?

4 个答案:

答案 0 :(得分:9)

TL;博士

在CPython中,itertools.tee是线程安全的当且仅当原始迭代器在C / C ++中实现时,即不使用任何蟒。

如果原始迭代器it是用python编写的,就像类实例或生成器一样,那么itertools.tee(it) 是线程安全的。在最好的情况下,你只会得到一个异常(你将会这样),并且在最糟糕的python中会崩溃。

这不是使用tee,而是一个线程安全的包装类和函数:

class safeteeobject(object):
    """tee object wrapped to make it thread-safe"""
    def __init__(self, teeobj, lock):
        self.teeobj = teeobj
        self.lock = lock
    def __iter__(self):
        return self
    def __next__(self):
        with self.lock:
            return next(self.teeobj)
    def __copy__(self):
        return safeteeobject(self.teeobj.__copy__(), self.lock)

def safetee(iterable, n=2):
    """tuple of n independent thread-safe iterators"""
    lock = Lock()
    return tuple(safeteeobject(teeobj, lock) for teeobj in tee(iterable, n))

我现在在tee是什么时候展开(很多)并且不是线程安全的,以及为什么。

好的例子

让我们运行一些代码(这是python 3代码,对于python 2使用itertools.izip而不是zip具有相同的行为):

>>> from itertools import tee, count
>>> from threading import Thread

>>> def limited_sum(it):
...     s = 0
...     for elem, _ in zip(it, range(1000000)):
...         s += elem
...     print(elem)

>>> a, b = tee(count())
>>> [Thread(target=limited_sum, args=(it,)).start() for it in [a, b]]
# prints 499999500000 twice, which is in fact the same 1+...+999999

itertools.count完全用C ++编写,在CPython项目的Modules/itertoolsmodule.c文件中,所以它工作得很好。

同样适用于:列表,元组,集合,范围,字典(键,值和项),collections.defaultdict(键,值和项)以及其他一些。

无法工作的示例 - 生成器

一个非常简短的例子是使用生成器:

>>> gen = (i for i in range(1000000))
>>> a, b = tee(gen)
>>> [Thread(target=sum, args=(it,)).start() for it in [a, b]]

Exception in thread Thread-10:
Traceback (most recent call last):
  File "/usr/lib/python3.4/threading.py", line 920, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.4/threading.py", line 868, in run
    self._target(*self._args, **self._kwargs)
ValueError: generator already executing

是的,tee是用C ++编写的,GIL一次只能执行一个字节的代码。但上面的例子表明,这还不足以确保线程安全。这就是发生的事情:

  1. 这两个主题在他们的tee_object实例上调用next的次数相同,
  2. 主题1调用next(a)
  3. 它需要获取一个新元素,因此线程1现在调用next(gen)
  4. gen是用python编写的。比方说,gen.__next__ CPython的第一个字节代码决定切换线程,
  5. 线程2恢复并调用next(b)
  6. 需要获取一个新元素,因此它会调用next(gen)
  7. 由于gen.__next__已在线程1中运行,因此我们会遇到异常。
  8. 无法工作的示例 - 迭代器对象

    好吧,也许在tee内使用生成器不是线程安全的。然后我们运行上面代码的变体,它使用迭代器对象:

    >>> from itertools import tee
    >>> from threading import Thread
    >>> class countdown(object):
    ...     def __init__(self, n):
    ...         self.i = n
    ...     def __iter__(self):
    ...         return self
    ...     def __next__(self):
    ...         self.i -= 1
    ...         if self.i < 0:
    ...             raise StopIteration
    ...         return self.i
    ... 
    >>> a, b = tee(countdown(100000))
    >>> [Thread(target=sum, args=(it,)).start() for it in [a, b]]
    Segmentation fault (core dumped)
    

    上面的代码在Ubuntu,Windows 7和OSX上的python 2.7.13和3.6(可能还有所有cpython版本)中崩溃了。我还不想透露原因,再过一步。

    如果我在迭代器中使用锁怎么办?

    可能上面的代码崩溃了,因为我们的迭代器本身不是线程安全的。让我们添加一个锁,看看会发生什么:

    >>> from itertools import tee
    >>> from threading import Thread, Lock
    >>> class countdown(object):
    ...     def __init__(self, n):
    ...         self.i = n
    ...         self.lock = Lock()
    ...     def __iter__(self):
    ...         return self
    ...     def __next__(self):
    ...         with self.lock:
    ...             self.i -= 1
    ...             if self.i < 0:
    ...                 raise StopIteration
    ...             return self.i
    ... 
    >>> a, b = tee(countdown(100000))
    >>> [Thread(target=sum, args=(it,)).start() for it in [a, b]]
    Segmentation fault (core dumped)
    

    在迭代器中添加一个锁是不足以使tee线程安全的。

    为什么tee不是线程安全的

    问题的关键是CPython文件getitem中的teedataobject Modules/itertoolsmodule.c方法。 tee的实现非常酷,通过优化可以节省RAM调用:tee返回&#34; tee对象&#34;,每个对象都保存对头teedataobject的引用。这些反过来就像链接列表中的链接,但不是持有单个元素 - 它们保持57.这对我们的目的来说并不重要,但它就是它的本质。以下是getitem的{​​{1}}函数:

    teedataobject

    当被要求提供元素时,static PyObject * teedataobject_getitem(teedataobject *tdo, int i) { PyObject *value; assert(i < LINKCELLS); if (i < tdo->numread) value = tdo->values[i]; else { /* this is the lead iterator, so fetch more data */ assert(i == tdo->numread); value = PyIter_Next(tdo->it); if (value == NULL) return NULL; tdo->numread++; tdo->values[i] = value; } Py_INCREF(value); return value; } 会检查是否有一个元素。如果是,则返回它。如果它没有,则它在原始迭代器上调用teedataobject。这是,如果迭代器是用python编写的,代码可以挂起。所以这就是问题所在:

    1. 两个线程调用next的次数相同,
    2. 线程1调用next,C代码进入上面的next(a)调用。比方说,PyIter_Next的第一个字节代码,CPython决定切换线程。
    3. 线程2调用next(gen),因为它仍然需要一个新元素,C代码会进入next(b)调用,
    4. 此时两个线程位于同一位置,PyIter_Nexti的值相同。请注意,tdo->numread只是一个变量,用于跟踪tdo->numread应写入下一个的57个单元格链接的位置。

      1. 线程2完成对teedataobject的调用并返回一个元素。在某些时候,CPython决定再次切换线程,
      2. 线程1恢复,完成对PyIter_Next的调用,然后运行两行:

        PyIter_Next
      3. 但是帖子2已经设置了 tdo->numread++; tdo->values[i] = value;

      4. 这已经足以表明tdo->values[i]不是线程安全的,因为我们丢失了线程2放在tee中的值。但这并不能解释崩溃。

        tdo->values[i]为56.由于两个线程都调用i,现在它变为58 - 高于57,分配的大小为tdo->numread++。在线程1继续移动之后,对象tdo->values没有更多引用,可以删除。这是tdo

        的明确功能
        teedataobject

        在标有&#34;问题&#34;的行上,CPython将尝试清除static int teedataobject_clear(teedataobject *tdo) { int i; PyObject *tmp; Py_CLEAR(tdo->it); for (i=0 ; i<tdo->numread ; i++) Py_CLEAR(tdo->values[i]); // <----- PROBLEM!!! tmp = tdo->nextlink; tdo->nextlink = NULL; teedataobject_safe_decref(tmp); return 0; } 。这是崩溃发生的地方。好吧,有些时候。碰撞的地方不止一个,我只想展示一个。

        现在您知道了 - tdo->values[57]不是线程安全的。

        一种解决方案 - 外部锁定

        我们可以锁定itertools.tee,而不是锁定我们的迭代器__next__。这意味着每次都会由单个线程调用整个tee.__next__方法。我在这个答案的开头做了一个简短的实现。它是线程安全的teedataobject.__getitem__的替代品。它没有实现tee所做的唯一事情就是腌制。由于锁定不可拣选,因此添加此锁定并非易事。但是,当然可以做到。

答案 1 :(得分:2)

如果文档中显示的等效代码,请执行以下操作:

是正确的,然后是,它不是线程安全的。

请注意,虽然deque被记录为具有线程安全的附加和弹出,但它不会对使用它的代码做出任何保证。

由于主代码最终可能会要求多个线程上的元素的底层迭代器,因此您需要使用线程安全的集合和迭代器作为输入,以使T恤安全。

答案 2 :(得分:0)

在C-Python中,itertools.tee()和它返回的迭代器是用C代码实现的。这意味着GIL应该保护它不被多个线程同时调用。它可能会正常工作,并且它不会使解释器崩溃,但不能保证是线程安全的。

简单地说,不要冒风险。

答案 3 :(得分:0)

我想分享一下使用itertools.tee在Python 3.6.9和3.7.4环境下将大型plat文本文件从s3拆分为多个csv文件的经验。

我的数据流来自s3 zip文件,s3fs读取iter,映射iter进行数据类转换,tee迭代,map iter进行数据类过滤器,遍历iter并捕获数据并使用s3fs write和/或以cv格式写入s3本地写入和s3fs放入s3。

在zipfile进程堆栈上,itertools.tee失败。

上文中,Dror Speiser的安全tee运行良好,但是由于tee数据集分布不佳或处理延迟,内存使用因tee对象之间的任何不平衡而增加了。 另外,它不能与多处理日志一起正常使用,可能与以下错误有关:https://bugs.python.org/issue34410

下面的代码只是在tee对象之间添加简单的流控制,以防止内存增加和OOM Killer情况。

希望对以后的参考很有帮助。

import time
import threading
import logging
from itertools import tee
from collections import Counter

logger = logging.getLogger(__name__)


FLOW_WAIT_GAP = 1000  # flow gap for waiting
FLOW_WAIT_TIMEOUT = 60.0  # flow wait timeout


class Safetee:
    """tee object wrapped to make it thread-safe and flow controlled"""

    def __init__(self, teeobj, lock, flows, teeidx):
        self.teeobj = teeobj
        self.lock = lock
        self.flows = flows
        self.mykey = teeidx
        self.logcnt = 0

    def __iter__(self):
        return self

    def __next__(self):
        waitsec = 0.0
        while True:
            with self.lock:
                flowgap = self.flows[self.mykey] - self.flows[len(self.flows) - 1]
                if flowgap < FLOW_WAIT_GAP or waitsec > FLOW_WAIT_TIMEOUT:
                    nextdata = next(self.teeobj)
                    self.flows[self.mykey] += 1
                    return nextdata

            waitthis = min(flowgap / FLOW_WAIT_GAP, FLOW_WAIT_TIMEOUT / 3)
            waitsec += waitthis

            time.sleep(waitthis)

            if waitsec > FLOW_WAIT_TIMEOUT and self.logcnt < 5:
                self.logcnt += 1
                logger.debug(f'tee wait seconds={waitsec:.2f}, mykey={self.mykey}, flows={self.flows}')

    def __copy__(self):
        return Safetee(self.teeobj.__copy__(), self.lock, self.flows, self.teeidx)


def safetee(iterable, n=2):
    """tuple of n independent thread-safe and flow controlled iterators"""
    lock = threading.Lock()
    flows = Counter()
    return tuple(Safetee(teeobj, lock, flows, teeidx) for teeidx, teeobj in enumerate(tee(iterable, n)))