concurrent.futures.ThreadPoolExecutor.map is slower than a for loop
我正在玩current.futures.ThreadPoolExecutor,以查看是否可以从四核处理器(具有8个逻辑核)中挤出更多工作。所以我写了下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | from concurrent import futures def square(n): return n**2 def threadWorker(t): n, d = t if n not in d: d[n] = square(n) def master(n, numthreads): d = {} with futures.ThreadPoolExecutor(max_workers=numthreads) as e: for i in e.map(threadWorker, ((i, d) for i in range(n))): pass # done so that it actually fetches each result. threadWorker has its own side-effects on d return len(d) if __name__ =="__main__": print('starting') print(master(10**6, 6)) print('done') |
有趣的是,相同的功能在for循环中编写大约需要一秒钟:
1 2 3 | >>> d = {} >>> for i in range(10**6): ... if i not in d: d[i] = i**2 |
...而线程池代码则需要10秒钟以上的时间。现在我知道它正在使用至少4个线程,因为我看到了每个内核上的处理器负载。但是,即使使用共享内存(由于内存复制,我也能理解为什么进程可能要花一些时间),但我觉得运行时的这种差距仍然太大。
有谁知道为什么要花这么长时间吗?似乎确实可以高度并行化的简单平方运算实际上应该不需要那么长时间。可能是由于字典的数量所引起的(如果是的话,是什么原因导致字典速度下降?)?
技术细节:
- Python 3.3.3
- 四核(8个带超标题的逻辑核)CPU
- MAC OSX 10.9.1(小牛)
我还没有尝试过期货,但是我相信它是基于线程的,因此这可能适用:
简而言之,与I / O绑定的工作负载在CPython中运行良好,但与CPU绑定的工作负载却没有。而且,如果您在同一进程中混合了受I / O约束的线程和受CPU约束的线程,那么它们也不是很好。
如果那是问题,我建议您增加工作块的大小(仅对一个数字进行平方就很小),并使用多处理。多重处理类似于线程,但是它使用具有共享内存的多个进程,并且比起线程化,趋向于在程序组件之间提供更宽松的耦合。
或切换到Jython或IronPython;这些据说线程很好。
您正在使用异步线程来尝试使CPU限制的工作并发吗?我不推荐它。请改用进程,否则GIL将随着线程池大小的增加而越来越慢。
[编辑1]
大卫·比兹利(David Beazly)(sp?)对GIL解释的类似问题。
Python代码的性能随线程而降低
线程有开销
与其他答案相反,我认为这里的罪魁祸首不是GIL(尽管这是一个问题),而是使用线程的开销。
生成和在系统级线程之间进行切换的开销很小(小于1ms),但仍可能使平方一个整数的开销不堪重负。理想情况下,在使用任何类型的并行性时,您都希望将计算分解为更大的部分(也许平方一百万个整数)。
绕过GIL
如果您使用数字Python堆栈(NumPy / Pandas / C / Fortran / Cython / Numba),则可以绕过GIL。例如,以下函数将对一组数字求平方并释放GIL。
1 2 3 4 5 6 7 8 9 10 | import numpy as np x = np.array(my_list) import numba @numba.jit(nogil=True) def square(x): for i in range(len(x)): x[i] = x[i]**2 return x |
或者,大多数Numpy操作会释放GIL
1 | x = x**2 |
记忆瓶颈
仅对整数进行平方运算时,任何系统都无法使用多个内核。您的CPU能够对整数求平方的速度远快于内存层次结构传递整数的速度。
Python具有全局解释器锁,该锁不允许在不同线程中同时执行同一进程的Python代码。
要实现真正的并行执行,您必须使用多个进程(易于切换至