繁体   English   中英

Python 在 Windows 上高效地进行多处理

[英]Python multiprocessing efficiently on Windows

假设我们将一个数字拆分为不同的域:例如:100 拆分为:[0, 25] [25, 50] [50, 75] [75, 100]。 然后,我们将这 4 个列表中的每一个发送到 4 个单独过程中的一个进行计算,然后将答案重新组合为数字 100 的单个拆分单元。我们根据流程的需要连续迭代多次' 以将 1000 个数字作为一个单位工作,这些数字被分成类似于 [0, 25] [25, 50] [50, 75] [75, 100] 的单独域。 如果我们必须关闭进程以使它们充当为答案而处理的单个组单元,则会出现效率问题。 由于 windows 与 Unix 相比对于运行进程来说是垃圾,因此我们被迫使用“spawn”方法而不是 fork。 spawn 方法在生成过程中很慢'所以我想为什么不保持进程'打开并从它们传递数据而不需要为并行过程的每个迭代组打开和关闭它们'。 下面的示例代码将执行此操作。 它将保持进程'打开为 Class 消费者将不断地使用 run()(在 while 循环中)请求带有 .get() 可加入队列的 next_task:

import multiprocessing


class Consumer(multiprocessing.Process):

    def __init__(self, task_queue, result_queue):
        multiprocessing.Process.__init__(self)
        self.task_queue = task_queue
        self.result_queue = result_queue

    def run(self):
        while True:
            next_task = self.task_queue.get()
            if next_task is None:
                # Poison pill shutdown of .get() loop with break
                self.task_queue.task_done()
                break
            answer = next_task()
            self.task_queue.task_done()
            self.result_queue.put(answer)
        return


class Task(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __call__(self):
        for i in range(self.b):
            if self.a % i == 0:
                return 0
        return 1


if __name__ == '__main__':
    # Establish communication queues
    tasks = multiprocessing.JoinableQueue()
    results = multiprocessing.Queue()

    # Number of consumers equal to system cpu_count
    num_consumers = multiprocessing.cpu_count() 
    
    # Make a list of Consumer object process' ready to be opened.
    consumers = [ Consumer(tasks, results) for i in range(num_consumers) ]

    for w in consumers:
        w.start()

    # Enqueue jobs for the Class Consumer process' run() while-loop to .get() a workload:
    num_jobs = 10
    for i in range(num_jobs):
        tasks.put(Task(i, 100)) # Similar jobs would be reiterated before poison pill.

    # We start to .get() the results in a different loop-
    for _ in range(num_jobs):  # -so the above loop enqueues all jobs without- 
        result = results.get() # -waiting for the previous .put() to .get() first.
   
    # Add a poison pill for each consumer
    for i in range(num_consumers): # We only do this when all computation is done.
        tasks.put(None) # Here we break all loops of open Consumer enqueue-able process'.

此代码只是一个示例。 在此代码的其他变体中:当实现 tasks.put() 和 results.get() 的更多迭代时,需要一种方法来使入队的 Task(object) 在完全计算答案之前通过外部调用返回并自行返回。 如果您已经从该单个拆分号码组的“其他进程”之一获得答案,这将释放资源。 __call__描述符需要存在,Task(object) 才能作为 function 调用 tasks.put(Task(i, 100))。 在过去的两周里,我一直在试图找出一种有效的方法来做到这一点。 我需要采取完全不同的方法吗? 不要误解我的困境,我正在使用有效的代码,但没有我在 Microsoft Windslows 上想要的效率。 任何帮助将不胜感激。

Task(object) 与将它入队的 Consumer() 进程不存在于同一进程中吗? 如果是这样,我不能告诉 Class Consumer() Run() 的所有进程'来停止他们当前正在运行的 Task(object) 而不关闭他们的 while 循环(使用毒丸),这样他们就可以立即接受另一个 Task() 而无需是否需要再次关闭并重新打开他们的流程? 当您为迭代计算打开和关闭数千个进程时,它确实会增加并浪费时间。 我曾尝试使用 Events() Managers() 其他 Queues()。 似乎没有一种有效的方法可以从外部干预 Task(object) 以立即return其父 Consumer() 以便在其他 Consumers() 之一返回的答案时它不会继续浪费资源计算使其他 Consumer() 任务的计算变得无关紧要,因为它们都作为单个数字的统一计算工作,并分成组。

您所做的是实现了自己的多处理池,但为什么呢? 您是否不知道concurrent.futures.ProcessPoolExecutormultiprocessing.pool.Pool类的存在,后者实际上更适合您的特定问题?

这两个类都实现了多处理池和用于将任务提交到池并从这些任务中返回结果的各种方法。 但是,由于在您的特定情况下,您提交的任务正在尝试解决相同的问题,并且您只对第一个可用结果感兴趣,一旦完成,您需要能够终止任何剩余的正在运行的任务。 只有multiprocessing.pool.Pool允许您这样做。

以下代码使用方法Pool.apply_async提交任务。 此 function 不会阻塞,而是返回一个AsyncResult实例,该实例具有阻塞的get方法,您可以调用该方法从提交的任务中获取结果。 但是,由于通常您可能会提交许多任务,我们不知道get调用这些实例中的哪一个。 因此,解决方案是改用apply_asynccallback参数,指定 function,只要任务可用,就会使用任务的返回值异步调用该参数。 然后问题就变成了传达这个结果。 有两种方法:

方法一:通过全局变量

from multiprocessing import Pool
import time


def worker1(x):
    time.sleep(3) # emulate working on the problem
    return 9 # the solution

def worker2(x):
    time.sleep(1) # emulate working on the problem
    return 9 # the solution

def callback(answer):
    global solution
    # gets all the returned results from submitted tasks
    # since we are just interested in the first returned result, write it to the queue:
    solution = answer
    pool.terminate() # kill all tasks


if __name__ == '__main__':
    t = time.time()
    pool = Pool(2) # just two processes in the pool for demo purposes
    # submit two tasks:
    pool.apply_async(worker1, args=(1,), callback=callback)
    pool.apply_async(worker2, args=(2,), callback=callback)
    # wait for all tasks to terminate:
    pool.close()
    pool.join()
    print(solution)
    print('Total elapsed time:', time.time() - t)

印刷:

9
Total elapsed time: 1.1378364562988281

方法2:通过队列

from multiprocessing import Pool
from queue import Queue
import time


def worker1(x):
    time.sleep(3) # emulate working on the problem
    return 9 # the solution

def worker2(x):
    time.sleep(1) # emulate working on the problem
    return 9 # the solution

def callback(solution):
    # gets all the returned results from submitted tasks
    # since we are just interested in the first returned result, write it to the queue:
    q.put_nowait(solution)


if __name__ == '__main__':
    t = time.time()
    q = Queue()
    pool = Pool(2) # just two processes in the pool for demo purposes
    # submit two tasks:
    pool.apply_async(worker1, args=(1,), callback=callback)
    pool.apply_async(worker2, args=(2,), callback=callback)
    # wait for first returned result from callback:
    solution = q.get()
    print(solution)
    pool.terminate() # kill all tasks in the pool
    print('Total elapsed time:', time.time() - t)

印刷:

9
Total elapsed time: 1.1355643272399902

更新

即使在 Windows 下,与任务完成所需的时间相比,创建和重新创建池的时间也可能相对微不足道,尤其是对于以后的迭代,即较大的n值。 如果你调用的是同一个worker function,那么第三种方法是使用池方法imap_unordered 我还包括一些代码来衡量我的桌面启动新池实例的开销是多少:

from multiprocessing import Pool
import time


def worker(x):
    time.sleep(x) # emulate working on the problem
    return 9 # the solution


if __name__ == '__main__':
    # POOLSIZE = multiprocessing.cpu_count()
    POOLSIZE = 8 # on my desktop
    # how long does it take to start a pool of size 8?
    t1 = time.time()
    for i in range(16):
        pool = Pool(POOLSIZE)
        pool.terminate()
    t2 = time.time()
    print('Average pool creation time: ', (t2 - t1) / 16)

    # POOLSIZE number of calls:
    arguments = [7, 6, 1, 3, 4, 2, 9, 6]
    pool = Pool(POOLSIZE)
    t1 = time.time()
    results = pool.imap_unordered(worker, arguments)
    it = iter(results)
    first_solution = next(it)
    t2 = time.time()
    pool.terminate()
    print('Total elapsed time:', t2 - t1)
    print(first_solution)

印刷:

Average pool creation time:  0.053139880299568176
Total elapsed time: 1.169790506362915
9

更新 2

这是一个难题:您有多个进程在处理一个难题。 例如,一旦一个进程发现一个数字可以被通过范围内的数字之一整除,那么在其他进程中测试不同范围内的数字以完成其测试就没有意义了。 你可以做三件事之一。 您可以什么都不做,让流程在开始下一次迭代之前完成。 但这会延迟下一次迭代。 我已经建议您终止进程,从而释放处理器。 但这需要您创建新的流程,而您认为这些流程并不令人满意。

我只能想到另一种可能性,我在下面使用您的多处理方法介绍了这种可能性。 一个名为stop的多处理共享 memory 变量被初始化为每个进程作为全局变量,并在每次迭代之前设置为 0。 当一个任务被设置为返回值 0 并且在其他进程中运行的其他任务没有任何意义时,它将stop的值设置为 1。这意味着任务必须定期检查stop的值,如果它有则返回已设置为 1。当然,这会为处理增加额外的周期。 在下面的演示中,我实际上有 100 个任务排队等待 8 个处理器。 但是最后 92 个任务会立即发现stop已设置并且应该在第一次迭代时返回。

顺便说一句:原始代码使用multiprocessing.JoinableQueue实例来对任务进行排队,而不是使用multiprocessing.Queue并在此实例上调用task_done ,因为消息已从队列中取出。 然而,从来没有在这个队列上发出过join的调用(它会告诉你什么时候所有的消息都被删除了),从而违背了拥有这样一个队列的全部目的。 事实上,不需要JoinableQueue ,因为主进程已经提交了num_jobs个作业,并且在结果队列中期待num_jobs消息,并且可以循环并从结果队列中提取预期数量的结果。 我用一个简单的Queue代替了JoinableQueue ,保留了原始代码,但注释掉了。 此外, Consumer进程可以创建为守护进程(使用参数daemon=True ),然后当所有非守护进程(即主进程)终止时它们将自动终止,从而消除了使用特殊“毒丸”的需要" None任务消息。 我已经进行了更改,并再次保持原始代码不变,但将其注释掉以进行比较。

import multiprocessing


class Consumer(multiprocessing.Process):

    def __init__(self, task_queue, result_queue, stop):
        # make ourself a daemon process:
        multiprocessing.Process.__init__(self, daemon=True)
        self.task_queue = task_queue
        self.result_queue = result_queue
        self.stop = stop

    def run(self):
        global stop
        stop = self.stop
        while True:
            next_task = self.task_queue.get()
            """
            if next_task is None:
                # Poison pill shutdown of .get() loop with break
                #self.task_queue.task_done()
                break
            """
            answer = next_task()
            #self.task_queue.task_done()
            self.result_queue.put(answer)
        # return


class Task(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __call__(self):
        global stop
        # start the range from 1 to avoid dividing by 0:
        for i in range(1, self.b):
            # how frequently should this check be made?
            if stop.value == 1:
                return 0
            if self.a % i == 0:
                stop.value = 1
                return 0
        return 1


if __name__ == '__main__':
    # Establish communication queues
    #tasks = multiprocessing.JoinableQueue()
    tasks = multiprocessing.Queue()
    results = multiprocessing.Queue()

    # Number of consumers equal to system cpu_count
    num_consumers = multiprocessing.cpu_count()

    # Make a list of Consumer object process' ready to be opened.
    stop = multiprocessing.Value('i', 0)
    consumers = [ Consumer(tasks, results, stop) for i in range(num_consumers) ]

    for w in consumers:
        w.start()

    # Enqueue jobs for the Class Consumer process' run() while-loop to .get() a workload:
    # many more jobs than processes, but they will stop immediately once they check the value of stop.value:
    num_jobs = 100
    stop.value = 0 # make sure it is 0 before an iteration
    for i in range(num_jobs):
        tasks.put(Task(i, 100)) # Similar jobs would be reiterated before poison pill.

    # We start to .get() the results in a different loop-
    results = [results.get() for _ in range(num_jobs)]
    print(results)
    print(0 in results)

    """
    # Add a poison pill for each consumer
    for i in range(num_consumers): # We only do this when all computation is done.
        tasks.put(None) # Here we break all loops of open Consumer enqueue-able process'.
    """

印刷:

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
True

我终于想出了一个解决办法!

import multiprocessing


class Consumer(multiprocessing.Process):

    def __init__(self, task_queue, result_queue, state):
        multiprocessing.Process.__init__(self)
        self.task_queue = task_queue
        self.result_queue = result_queue
        self.state = state


    def run(self):
        while True:
            next_task = self.task_queue.get()
            if next_task is None:
                self.task_queue.task_done()
                break
            # answer = next_task() is where the Task object is being called.
            # Python runs on a line per line basis so it stops here until assigned.
            # Put if-else on the same line so it quits calling Task if state.is.set()
            answer = next_task() if self.state.is_set() is False else 0
            self.task_queue.task_done()
            self.result_queue.put(answer)
        return


class Task(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __call__(self):
        for i in range(self.b):
            if self.a % i == 0:
                return 0
        return 1


def initialize(n_list, tasks, results, states):
    sum_list = []
    for i in range(cpu_cnt):
    tasks.put(Task(n_list[i], number))
    for _ in range(cpu_cnt):
        sum_list.append(int(results.get()))
        if 0 in sum_list:
            states.set()
    if 0 in sum_list:
        states.clear()
        return None
    else:
        states.clear()
        return number


if __name__ == '__main__':
    states = multiprocessing.Event() # ADD THIS BOOLEAN FLAG EVENT!
    tasks = multiprocessing.JoinableQueue()
    results = multiprocessing.Queue()

    cpu_cnt = multiprocessing.cpu_count() 

    # Add states.Event() to Consumer argument list:
    consumers = [ Consumer(tasks, results, states) for i in range(cpu_cnt) ]

    for w in consumers:
        w.start()

    n_list = [x for x in range(1000)]
    iter_list = []
    for _ in range(1000):
        iter_list.append(initialize(n_list, tasks, results, states)


    for _ in range(num_jobs):
        result = results.get()
   

    for i in range(num_consumers):
        tasks.put(None) 

如果打开的 Consumer 对象将答案分配给 next_task() function 调用并在同一行上使用 if-else 语句,那么它将在 state.Event() 标志设置时退出,因为它锁定到该行直到变量“ answer”由排队的任务 object 分配。 这是一个很好的解决方法。 它使任务 object 在通过变量“答案”分配运行它的消费者的 while 循环中可中断,超过 2 周,我找到了一个加快速度的解决方案! 我在代码的工作版本上对其进行了测试,它更快! 使用这样的方法,可以无限期地打开多个进程并通过消费者 object 可连接队列循环传递许多不同的任务对象,并以惊人的速度并行处理大量数据! 它几乎就像这段代码一样,使所有内核 function 像一个“超级内核”,其中所有进程对每个内核保持开放,并为任何所需的 i/o 迭代 stream 一起工作!

这是我的 python 多处理程序之一的示例 output 在 8 个超线程内核上实现此方法:

Enter prime number FUNCTION:n+n-1
Enter the number for 'n' START:1
Enter the number of ITERATIONS:100000
Progress: ########## 100%
Primes:
ƒ(2) = 3
ƒ(3) = 5
ƒ(4) = 7
ƒ(6) = 11
ƒ(7) = 13
ƒ(9) = 17

etc etc...

ƒ(99966) = 199931
ƒ(99967) = 199933
ƒ(99981) = 199961
ƒ(99984) = 199967
ƒ(100000) = 199999
Primes found: 17983
Prime at end of list has 6 digits.
Overall process took 1 minute and 2.5 seconds.

从 1 到 200,000 的所有 17983 个素数(#2 除外)全模在 ~ 1 分钟内

在 3990x 128 线程 AMD Threadripper 上,大约需要 8 秒。

这是另一个在 8 个超线程内核上的 output:

Enter prime number FUNCTION:((n*2)*(n**2)**2)+1
Enter the number for 'n' START:1
Enter the number of ITERATIONS:1000
Progress: ########## 100%
Primes:
ƒ(1) = 3
ƒ(3) = 487
ƒ(8) = 65537
    
    etc... etc...
    
ƒ(800) = 655360000000001
ƒ(839) = 831457011176399
ƒ(840) = 836423884800001
ƒ(858) = 929964638281537
ƒ(861) = 946336852720603
ƒ(884) = 1079670712526849
ƒ(891) = 1123100229130903
ƒ(921) = 1325342566697203
ƒ(953) = 1572151878119987
ƒ(959) = 1622269605897599
ƒ(983) = 1835682572370287
Primes found: 76
Prime at end of list has 16 digits.
Overall process took 1 minute and 10.6 seconds.

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM