关于python:使用线程将数组切成块并在每个块上执行计算并将返回的数组重新组装为一个数组

Using threading to slice an array into chunks and perform calculation on each chunk and reassemble the returned arrays into one array

我有一个大的python数组,我想分成多个块,然后对这些块执行计算,然后"重新组装"为一个数组。 到目前为止,这里是我到目前为止刚开始学习线程和Python线程的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def performCalc(binaryArray):

    # perform some operation
    rArray = blah * binaryArray
    return rArray

def main(argv):
    numberOfThreads = 5
    str(len(grey_arr) # 100,000 elements
    greyScaleChunks = np.array_split(grey_arr, numberOfThreads)
    for i in range(numberOfThreads):
        t = Thread(target=performCalc, args=(greyScaleChunks[i],))
        t.start()

    # take all the return values and later create one big array to be resized into matrix.

块的顺序很重要,我必须保持这一点。


如果要使用显式的Thread对象解决该问题,并希望获得线程函数的结果,则需要保留这些Thread对象,以便以后可以join对象并提取其结果。像这样:

1
2
3
4
5
6
7
8
ts = []
for i in range(numberOfThreads):
    t = Thread(target=performCalc, args=(greyScaleChunks[i],))
    ts.append(t)
    t.start()
for t in ts:
    t.join()
# When you get here, all threads have finished

同样,Thread.run的默认实现只是调用您的target并丢弃结果。因此,您需要将返回值存储在主线程可以访问的位置。许多numpy程序通过在每个线程中传递一个预分配的数组来做到这一点,因此它们可以填充它们,这对您的设计没有太大的改变,但这并不是您前进的方向。当然,您可以传入任何其他可变对象进行突变。或设置一个全局变量,等等。但是,您已经围绕返回值进行了设计,这是思考问题的一种好方法,所以让我们坚持下去。使该工作最简单的方法是子类Thread

1
2
3
4
5
6
7
8
9
10
11
class ReturningThread(threading.Thread):
    def run(self):
        try:
            if self._target:
                self._result = self._target(*self._args, **self._kwargs)
        finally:
            del self._target, self._args, self._kwargs

    def join(self):
        super().join()
        return self._result

这是未经测试的代码,但是应该可以工作。 (我已经在真实代码中做过类似的事情,但是更加复杂,以允许join正确处理超时;这里我保持简单,只在run方法中添加_result =return对其进行了处理。在join中。)

所以:

1
2
3
4
5
6
7
8
ts = []
for i in range(numberOfThreads):
    t = ReturningThread(target=performCalc, args=(greyScaleChunks[i],))
    ts.append(t)
    t.start()
results = []
for t in ts:
    results.append(t.join())

现在,您有了可以堆叠在一起的阵列列表。

但是,我在上面所做的基本上是将每个线程变成半途而废的未来。仅使用实际的期货在概念上可能会更简单。这确实意味着我们现在正在使用一个我们实际上并不需要的线程池,每个线程只有一个任务。性能成本可能可以忽略不计(您在实际工作上花费的时间比在队列上花费的时间多,或者您不想首先以这种方式进行线程化),但是,更重要的是,我们要增加隐藏在引擎盖下(在经过良好测试的stdlib模块中)的额外复杂性,以减少我们代码的复杂性;是否值得,取决于您。无论如何:

1
2
with concurrent.futures.ThreadPoolExecutor(max_workers=numberOfThreads) as x:
    results = x.map(performCalc, greyScaleChunks)

这将处理创建5个线程,为每个performCalc(chunk)创建一个作业,将5个作业划分为5个线程,联接线程并按顺序收集5个作业的结果,因此您要做的就是将结果堆叠起来。

使用执行程序的另一个好处是,如果事实证明您的代码由于GIL而没有从线程并行中受益(在您的情况下不太可能是一个问题-您应该将大部分时间花在20000以上的numpy操作上)行,这些行将在发布GIL时运行-但显然您必须进行测试以验证这是正确的),您可以非常轻松地切换到进程:只需将ThreadPoolExecutor更改为ProcessPoolExecutor就可以了。

您的args和return可能无法以默认方式在进程之间复制或共享,或者这样做的代价太高了,以至于丧失了并行性的所有好处-但事实是您可以用一个单词来测试进行更改,然后仅在有问题的情况下进行处理,这仍然是一个胜利。


您可以使用很大程度上未公开说明的ThreadPool(在此答案中提到)及其map_async()方法来实现,如以下可运行示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import numpy as np
from pprint import pprint
from multiprocessing.pool import ThreadPool
import threading

blah = 2

def performCalc(binaryArray):
    # perform some operation
    rArray = blah * binaryArray
    return rArray

def main(data_array):
    numberOfThreads = 5
    pool = ThreadPool(processes=numberOfThreads)

    greyScaleChunks = np.array_split(data_array, numberOfThreads)
    results = pool.map_async(performCalc, greyScaleChunks)
    pool.close()
    pool.join()  # Block until all threads exit.

    # Final results will be a list of arrays.
    pprint(results.get())

grey_arr = np.array(range(50))
main(grey_arr)

打印结果:

1
2
3
4
5
[array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18]),
 array([20, 22, 24, 26, 28, 30, 32, 34, 36, 38]),
 array([40, 42, 44, 46, 48, 50, 52, 54, 56, 58]),
 array([60, 62, 64, 66, 68, 70, 72, 74, 76, 78]),
 array([80, 82, 84, 86, 88, 90, 92, 94, 96, 98])]