假设我有这个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()在访问这个共享数据时会进行适当的锁定吗?
答案 0 :(得分:9)
在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一次只能执行一个字节的代码。但上面的例子表明,这还不足以确保线程安全。这就是发生的事情:
next
的次数相同,next(a)
,next(gen)
,gen
是用python编写的。比方说,gen.__next__
CPython的第一个字节代码决定切换线程,next(b)
,next(gen)
gen.__next__
已在线程1中运行,因此我们会遇到异常。好吧,也许在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
线程安全的。
问题的关键是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编写的,代码可以挂起。所以这就是问题所在:
next
的次数相同,next
,C代码进入上面的next(a)
调用。比方说,PyIter_Next
的第一个字节代码,CPython决定切换线程。next(gen)
,因为它仍然需要一个新元素,C代码会进入next(b)
调用,此时两个线程位于同一位置,PyIter_Next
和i
的值相同。请注意,tdo->numread
只是一个变量,用于跟踪tdo->numread
应写入下一个的57个单元格链接的位置。
teedataobject
的调用并返回一个元素。在某些时候,CPython决定再次切换线程,线程1恢复,完成对PyIter_Next
的调用,然后运行两行:
PyIter_Next
但是帖子2已经设置了 tdo->numread++;
tdo->values[i] = value;
!
这已经足以表明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)))