【问题标题】:Python multi-threaded processing with limited CPU/ports具有有限 CPU/端口的 Python 多线程处理
【发布时间】:2014-12-25 12:20:17
【问题描述】:

我有一个文件夹名称字典,我想并行处理。在每个文件夹下,有一个我想在 series 中处理的文件名数组:

folder_file_dict = {
         folder_name : {
                         file_names_key : [file_names_array]
                       }
        }

最终,我将创建一个名为 folder_name 的文件夹,其中包含名称为 len(folder_file_dict[folder_name][file_names_key]) 的文件。我有这样的方法:

def process_files_in_series(file_names_array, udp_port):
    for file_name in file_names_array:
         time_consuming_method(file_name, udp_port)
         # create "file_name"

udp_ports = [123, 456, 789]

请注意上面的time_consuming_method(),由于通过 UDP 端口调用需要很长时间。我也仅限于使用上面数组中的 UDP 端口。因此,我必须等待 time_consuming_method 在 UDP 端口上完成,然后才能再次使用该 UDP 端口。这意味着我一次只能运行len(udp_ports) 线程。

因此,我最终将创建len(folder_file_dict.keys()) 线程,并通过len(folder_file_dict.keys()) 调用process_files_in_series。我也有一个 MAX_THREAD 计数。我正在尝试使用QueueThreading 模块,但我不确定我需要什么样的设计。我如何使用队列和线程以及可能的条件来做到这一点?使用线程池的解决方案也可能会有所帮助。

注意

我并不是想提高读/写速度。我正在尝试并行化对process_files_in_series 下的time_consuming_method 的调用。创建这些文件只是过程的一部分,而不是速率限制步骤。

另外,我正在寻找使用QueueThreading 和可能的Condition 模块或与这些模块相关的任何内容的解决方案。线程池解决方案也可能会有所帮助。我不能使用进程,只能使用线程。

我也在寻找 Python 2.7 中的解决方案。

【问题讨论】:

  • 最简单的解决方案(代码方面)是使用线程池,例如multiprocessing.dummy.Pool.map(),这里是code example。为什么要并行处理文件?如果所有文件名都在同一个物理磁盘上;并行处理可能不会提高时间性能(可能相反)。另一方面,如果进程受 CPU 限制,那么您应该使用进程而不是线程(如果 process_files_in_series() 不释放 GIL)。
  • process_files_in_series 方法中处理每个文件需要很长时间。因此,我试图通过为每个文件夹并行调用一次来并行化process_files_in_series 上的调用。
  • 如果您的磁盘只能以 100MB/s 的速度读/写,那么如果您的代码已经以 100MB/s 的速度读/写,那么再多的线程也不会让您的代码更快。
  • 我的帖子在上面更正了。我的写入速度没有接近最大值,因为我需要在process_files_in_series 内执行一个很长的方法。抱歉,如果它具有误导性,但我并不想提高我的读/写速度。
  • 如果只有一个内核,那么并行运行受 CPU 限制的代码(计算)可能不会运行得更快。线程和进程都无济于事。

标签: python multithreading python-2.7 parallel-processing


【解决方案1】:

这是如何使用 multiprocessing.Process 的蓝图 使用 JoinableQueue 将作业交付给工人。你会 仍然受 I/O 约束,但使用 Process 你确实有真正的并发, 这可能被证明是有用的,因为线程甚至可能比 处理文件的普通脚本。

(请注意,这也会阻止您使用笔记本电脑做任何其他事情 如果你敢一次启动太多进程:P)。

我试图解释代码 尽可能使用 cmets。

import traceback

from multiprocessing import Process, JoinableQueue, cpu_count

# Number if CPU's on your PC
cpus = cpu_count()

# The Worker Function. Could also be modelled as a class
def Worker(q_jobs):
    while True:
        # Try / Catch / finally may be necessary for error-prone tasks since the processes 
        # may hang forever if the task_done() method is not called.
        try:
            # Get an item from the Queue
            item = q_jobs.get()

            # At this point the data should somehow be processed

        except:
            traceback.print_exc()
        else:
            pass

        finally:
            # Inform the Queue that the Task has been done
            # Without this. The processes can not be killed
            # and will be left as Zombies afterwards
            q_jobs.task_done()


# A Joinable Queue to end the process
q_jobs = JoinableQueue()

# Create process depending on the number of CPU's
for i in range(cpus):

    # target function and arguments
    # a list of multiple arguments should not end with ',' e.g.
    # (q_jobs, 'bla')
    p = Process(target=Worker,
                args=(q_jobs,)
                )
    p.daemon = True
    p.start()

# fill Queue with Jobs
q_jobs.put(['Do'])
q_jobs.put(['Something'])

# End Process
q_jobs.join()

干杯

编辑

我写这篇文章时考虑到了 Python 3。 从 print 函数中取出括号

print item

应该使这项工作适用于 2.7。

【讨论】:

  • 如果处理任何项目可能导致错误,所编写的代码可能会永远挂起。您不应该在没有同步的情况下从多个线程/进程打印到同一个地方。如果simpler pool-based code is used 可以避免这两个问题:请注意代码捕获并报告异常,并且仅在主线程中打印。
  • 带有 finally task_done 的 try 块是否不足以解决这个问题?我还不能说我明白你的意思。到目前为止,这对我来说效果很好。
  • 好吧,我将编辑它,但打印不会失败。由于消息将被缓冲线程安全我猜:P。因此冲洗,因为打印可能不会显示。不过感谢您的更新。 :) 这样应该没有任何问题。
  • 重新引发异常也可能导致最终的死锁(据我所知,未捕获的异常会杀死线程)。如果你正在重新实现一个线程池;你需要关心这些事情(例如,重生被杀死的线程)。顺便提一句。要在 except 块内重新引发异常,只需编写:raise(无括号,无参数)。
  • 刷新内部缓冲区只有在您 print data that is less than PIPE_BUF in size 时才有帮助。
【解决方案2】:

使用线程池:

#!/usr/bin/env python2
from multiprocessing.dummy import Pool, Queue # thread pool

folder_file_dict = {
    folder_name: {
        file_names_key: file_names_array
    }
}

def process_files_in_series(file_names_array, udp_port):
    for file_name in file_names_array:
         time_consuming_method(file_name, udp_port)
         # create "file_name"
         ...

def mp_process(filenames):
    udp_port = free_udp_ports.get() # block until a free udp port is available
    args = filenames, udp_port
    try:
        return args, process_files_in_series(*args), None
    except Exception as e:
        return args, None, str(e)
    finally:
        free_udp_ports.put_nowait(udp_port)

free_udp_ports = Queue() # in general, use initializer to pass it to children
for port in udp_ports:
    free_udp_ports.put_nowait(port)
pool = Pool(number_of_concurrent_jobs) #
for args, result, error in pool.imap_unordered(mp_process, get_files_arrays()):
    if error is not None:
       print args, error

如果不同文件名数组的处理时间可能不同,我认为您不需要将线程数绑定到 udp 端口​​数。

如果我正确理解folder_file_dict 的结构,则生成文件名数组:

def get_files_arrays(folder_file_dict=folder_file_dict):
    for folder_name_dict in folder_file_dict.itervalues():
        for filenames_array in folder_name_dict.itervalues():
            yield filenames_array

【讨论】:

  • @J.F.Sebastion 感谢您的帮助,但我怎么知道哪个 udp 端口​​可用?我认为最好仅在线程完成时切换端口......否则我可能无法检测哪个端口何时可用。
  • @Lucas:如果没有其他方法可以检测到(例如,如果端口可用,则执行成功的非阻塞操作),您可以使用空闲 udp 端口​​队列。调用udp_port = queue.get() 获取端口,在mp_process 末尾的finally 子句中调用queue.put_nowait(udp_port) 以释放它。有multiprocessing.dummy.Queue类。
  • @J.F.Sebastion 你太棒了。这个解决方案非常有帮助。谢谢!
【解决方案3】:

使用multiprocessing.pool.ThreadPool。它为您处理队列/线程管理,并且可以轻松更改为进行多处理。

编辑:添加示例

这是一个示例...多个线程可能最终使用相同的 udp 端口​​。我不确定这对你来说是否有问题。

import multithreading
import multithreading.pool
import itertools

def process_files_in_series(file_names_array, udp_port):
    for file_name in file_names_array:
         time_consuming_method(file_name, udp_port)
         # create "file_name"

udp_ports = [123, 456, 789]

folder_file_dict = {
         folder_name : {
                         file_names_key : [file_names_array]
                       }
        }

def main(folder_file_dict, udp_ports):
    # number of threads - here I'm limiting to the smaller of udp_ports,
    # file lists to process and a cap I arbitrarily set to 4
    num_threads = min(len(folder_file_dict), len(udp_ports), 4)
    # the pool
    pool = multithreading.pool.ThreadPool(num_threads)
    # build files to be processed into list. You may want to do other
    # Things like join folder_name...
    file_arrays = [value['file_names_key'] for value in folder_file_dict.values()]
    # do the work
    pool.map(process_files_in_series, zip(file_arrays, itertools.cycle(udp_ports))
    pool.close()
    pool.join()

【讨论】:

  • 您能否概述使用 ThreadPool 的解决方案? (理想情况下使用 Python 2.7?)。我有一个问题,由于 UDP 端口限制,我只能使用几个线程(上面更新了答案)
  • 如果多个线程使用同一个UDP端口;它有效地对它们进行序列化(一次只能运行一个线程):“因此,我必须等待 time_sumption_method 在 UDP 端口上完成,然后才能再次使用该 UDP 端口”。如果你想为每个线程分配它自己的 udp 端口​​,你可以使用threading.local()。或者编写一个函数来选择第一个可用端口as I've suggested
  • @J.F.Sebastion 很好的建议,我也会研究threading.local()
猜你喜欢
  • 1970-01-01
  • 2021-08-15
  • 1970-01-01
  • 1970-01-01
  • 2021-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多