我对我编写的一些代码感到非常困惑。我很惊讶地发现:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(f, iterable))
和
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
results = list(map(lambda x: executor.submit(f, x), iterable))
产生不同的结果。第一个生成f
返回类型的列表,第二个生成concurrent.futures.Future
对象列表,然后需要使用result()
方法进行评估,以获得f
方法的值{1}}已退回。
我主要担心的是,这意味着executor.map
无法利用concurrent.futures.as_completed
,这似乎是一种非常方便的方法来评估一些长时间运行的数据库调用的结果他们变得可用时正在制作。
我对concurrent.futures.ThreadPoolExecutor
对象的工作方式一点都不清楚 - 天真地,我更喜欢(稍微冗长一点):
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
result_futures = list(map(lambda x: executor.submit(f, x), iterable))
results = [f.result() for f in futures.as_completed(result_futures)]
更简洁executor.map
,以便利用可能的性能提升。我错了吗?
答案 0 :(得分:19)
问题是您将ThreadPoolExecutor.map
的结果转换为列表。如果你不这样做而是直接迭代生成的生成器,结果仍然以正确的顺序产生,但循环在所有结果准备好之前继续。您可以使用此示例对此进行测试:
import time
import concurrent.futures
e = concurrent.futures.ThreadPoolExecutor(4)
s = range(10)
for i in e.map(time.sleep, s):
print(i)
保留订单可能是因为有时候你得到的结果与你给他们映射的顺序相同是很重要的。并且结果可能未包含在将来的对象中,因为在某些情况下,如果需要,可能需要花费很长时间才能在列表上执行另一个映射以获取所有结果。毕竟在大多数情况下,在循环处理第一个值之前,下一个值很可能就绪了。这个例子证明了这一点:
import concurrent.futures
executor = concurrent.futures.ThreadPoolExecutor() # Or ProcessPoolExecutor
data = some_huge_list()
results = executor.map(crunch_number, data)
finals = []
for value in results:
finals.append(do_some_stuff(value))
在这个示例中,do_some_stuff
可能需要比crunch_number
更长的时间,如果确实如此,那么在保持地图的简单使用的同时,性能确实不会大幅下降。< / p>
此外,由于工作线程(/ processes)在列表的开头处开始处理并且按照你提交的列表的结尾工作,结果应该按照它们已经由迭代器产生的顺序完成。这意味着在大多数情况下executor.map
都很好,但在某些情况下,例如,如果您处理值的顺序无关紧要,而传递给map
的函数则需要非常不同的时间运行,future.as_completed
可能会更快。
答案 1 :(得分:8)
如果使用concurrent.futures.as_completed
,则可以处理每个函数的异常。
import concurrent.futures
iterable = [1,2,3,4,6,7,8,9,10]
def f(x):
if x == 2:
raise Exception('x')
return x
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
result_futures = list(map(lambda x: executor.submit(f, x), iterable))
for future in concurrent.futures.as_completed(result_futures):
try:
print('resutl is', future.result())
except Exception as e:
print('e is', e, type(e))
# resutl is 3
# resutl is 1
# resutl is 4
# e is x <class 'Exception'>
# resutl is 6
# resutl is 7
# resutl is 8
# resutl is 9
# resutl is 10
在executor.map
中,如果有异常,则整个执行器将停止。您需要在辅助函数中处理异常。
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
for each in executor.map(f, iterable):
print(each)
# if there is any exception, executor.map would stop
答案 2 :(得分:3)
下面是提交与地图的示例。他们俩都立即接受工作(提交-开始)。他们花费相同的时间完成11秒(最后结果时间-开始)。但是,只要ThreadPoolExecutor maxThreads = 2中的任何线程完成,submit就会给出结果。地图按提交顺序给出结果。
import time
import concurrent.futures
def worker(i):
time.sleep(i)
return i,time.time()
e = concurrent.futures.ThreadPoolExecutor(2)
arrIn = range(1,7)[::-1]
print arrIn
f = []
print 'start submit',time.time()
for i in arrIn:
f.append(e.submit(worker,i))
print 'submitted',time.time()
for r in concurrent.futures.as_completed(f):
print r.result(),time.time()
print
f = []
print 'start map',time.time()
f = e.map(worker,arrIn)
print 'mapped',time.time()
for r in f:
print r,time.time()
输出:
[6, 5, 4, 3, 2, 1]
start submit 1543473934.47
submitted 1543473934.47
(5, 1543473939.473743) 1543473939.47
(6, 1543473940.471591) 1543473940.47
(3, 1543473943.473639) 1543473943.47
(4, 1543473943.474192) 1543473943.47
(1, 1543473944.474617) 1543473944.47
(2, 1543473945.477609) 1543473945.48
start map 1543473945.48
mapped 1543473945.48
(6, 1543473951.483908) 1543473951.48
(5, 1543473950.484109) 1543473951.48
(4, 1543473954.48858) 1543473954.49
(3, 1543473954.488384) 1543473954.49
(2, 1543473956.493789) 1543473956.49
(1, 1543473955.493888) 1543473956.49
答案 3 :(得分:3)
除了此处答案中的解释之外,直接访问源也可能有所帮助。它重申此处另一个答案的说法:
.map()
按提交顺序给出结果,而concurrent.futures.as_completed()
在Future
个对象的列表上进行迭代并不能保证这种排序,因为这是as_completed()
的本质 .map()
在基类concurrent.futures._base.Executor
中定义:
class Executor(object):
def submit(self, fn, *args, **kwargs):
raise NotImplementedError()
def map(self, fn, *iterables, timeout=None, chunksize=1):
if timeout is not None:
end_time = timeout + time.monotonic()
fs = [self.submit(fn, *args) for args in zip(*iterables)] # <!!!!!!!!
def result_iterator():
try:
# reverse to keep finishing order
fs.reverse() # <!!!!!!!!
while fs:
# Careful not to keep a reference to the popped future
if timeout is None:
yield fs.pop().result() # <!!!!!!!!
else:
yield fs.pop().result(end_time - time.monotonic())
finally:
for future in fs:
future.cancel()
return result_iterator()
正如您所提到的,还有.submit()
,剩下的要在子类ProcessPoolExecutor
和ThreadPoolExecutor
中定义,并返回一个_base.Future
实例,您需要调用.result()
才能真正执行任何操作。
从.map()
开始的重要内容可以归结为:
fs = [self.submit(fn, *args) for args in zip(*iterables)]
fs.reverse()
while fs:
yield fs.pop().result()
.reverse()
加.pop()
的一种方法是首先产生(来自iterables
的第一次提交的结果,然后产生第二个提交的结果,依此类推)上。结果迭代器的元素不是Future
;它们本身就是实际结果。