concurrent.futures.ThreadPoolExecutor.map比for循环慢。

11

我正在使用concurrent.futures.ThreadPoolExecutor进行实验,看看能否从我的四核处理器(具有8个逻辑核)中挤出更多的工作。因此,我编写了以下代码:

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循环写出来只需要约一秒钟的时间:
>>> 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(Mavericks)

2
两个注释:1)由于GIL的存在,线程在这里不会提高性能,正如下面的答案所述;2)进程间通信并不便宜(而且可能变得非常复杂)。因此,最好编写您的工作程序以完全避免共享状态-而不是传递他们都要写入的dict,只需让每个人返回一个dict。或者类似的东西。 - roippi
4个回答

4

线程有开销

与其他答案不同,我认为主要问题不是GIL(虽然这确实是一个问题),而是使用线程的开销。

生成和切换系统级线程的开销很小(少于1毫秒),但仍然可能压倒了对单个整数求平方的成本。理想情况下,在使用任何形式的并行处理时,应将计算分解成更大的部分(例如平方一百万个整数)。

绕过GIL

如果使用数字Python栈(NumPy/Pandas/C/Fortran/Cython/Numba),可以绕过GIL。例如,以下函数将对数组中的数字进行平方,并释放GIL。

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)

x = x**2

内存瓶颈

如果只是对整数进行平方运算,任何系统都无法利用多个核心。你的CPU可以更快地计算整数平方,而内存层次结构却无法快速传递数据。


2
你正在使用异步线程来尝试并发地处理CPU密集型工作?我不建议这样做。使用进程代替,否则随着线程池大小的增加,全局解释器锁(GIL)会导致速度变得越来越慢。

[编辑1]

类似的问题,引用了David Beazly(拼写可能有误)对GIL的解释。

Python代码性能与线程降低


2
我还没有尝试过futures,但我相信它是基于线程的,所以这可能适用: http://www.youtube.com/watch?v=ph374fJqFPE 简而言之,在CPython中,I/O绑定的工作负载可以很好地进行线程处理,但CPU绑定的工作负载则不行。如果在同一进程中混合使用I/O绑定和CPU绑定的线程,也不会线程化。
如果这是问题,我建议增加您的工作块的大小(仅平方一个数字非常小),并使用multiprocessing。多处理类似于线程,但它使用具有共享内存的多个进程,并且通常比线程提供程序组件之间更松散的耦合。
或者,切换到Jython或IronPython;据说它们线程化得很好。

我明白多进程编程中有一个队列,它是各个进程之间共享的内存。是否有一种类似于队列但却是字典结构的东西呢? - inspectorG4dget
1
https://dev59.com/EWw15IYBdhLWcg3wGn5c - dstromberg
说多处理具有共享内存是误导性的,因为默认情况下,每个进程都有自己的进程控制块和独立的内存,但是如果您想执行进程间通信,则通过队列来完成,这充当共享内存的资源。 - Syed Shaharyaar Hussain
1
@SyedShaharyaarHussain 实际上,多进程有许多数据类型存储在共享内存中。例如:https://docs.python.org/3/library/multiprocessing.shared_memory.html - dstromberg

1
Python有一个全局解释器锁(GIL),它不允许在不同线程中同时执行同一进程的Python代码。为了实现真正的并行执行,您必须使用多个进程(可以轻松切换到ProcessPoolExecutor)或者本地(非Python,如C)代码。

ProcessPoolExecutor也需要很长时间。大约10秒后,我不得不手动终止它。 - inspectorG4dget
1
哦,@inspectorG4dget...我认为你做错了。我很确定这个map不能有副作用。你意识到你每次都在进程之间复制这个字典吗?难怪它会变慢... - Oleh Prypin
糟糕!我老是忘记了 XD。 - inspectorG4dget

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接