Concurrent.futures使用指南 - 使用线程和处理的简单示例

时间:2013-12-26 10:36:19

标签: python multithreading python-3.x

我想使用concurrent.futures模块启用程序的并行处理/线程化。

不幸的是,我似乎无法找到使用concurrent.futures模块的任何好的,简单的,笨拙的例子。它们通常需要更高级的python知识或处理/线程概念和行话。

下面是一个基于我的程序的简化的自包含示例:有一个纯粹的CPU绑定任务,适用于多路径,以及插入数据库(SQLite)的单独IO绑定任务。 在我的程序中,我已经将其转换为使用多处理池类,但由于CPU绑定任务的结果都被收集起来等待任务完成,因此它使用了大量内存。 因此,我希望使用线程/处理的组合,我相信concurrent.futures可以相当简单地为我做。

那么如何将以下内容转换为使用此模块的内容?

import sqlite3

#Stand in CPU intensive task
def calculate(value):
    return value * 10

#Stand in Thread I/O intensive task
def output(value):
    global db

    if (value % 1000) == 0:
        db.execute('delete from test_table')

    db.execute('insert into test_table (result) values (?)', (value,))

def main():
    global db
    results = []

    db  = sqlite3.connect('e:\\z_dev\\test.sqlite')
    db.cursor()

    #=========
    #Perform CPU intensive task
    for i in range(1000):
        results.append( calculate(i))

    #Perform Threading intensive task
    for a in results:
        output(a)
    #=========

    db.commit()
    db.close()

if __name__ == '__main__':
    main()

我正在寻找一个不使用任何花哨/复杂python的答案。或者一个很好的清晰简单的解释,或理想的两个!

由于

编辑:我当前的“多处理器”实现。可能错了,但似乎有效。没有任何线程。这是在上面的“#=========”部分内。

#Multiprocessing
pool = multiprocessing.Pool(None)
for i in range(1000):
    results.append( pool.apply_async(calculate(i)))
pool.close()
pool.join()

for i in results:
    results[i] = results[i].get()

#Complete lack of threading; but if I had it, it'd be here:     
for a in results:
    output(a)

1 个答案:

答案 0 :(得分:13)

concurrent.futures有一个简约的API。它很容易用于非常简单的问题,但是你没有一个非常简单的问题。如果你这样做了,你就已经解决了它; - )

你没有显示你编写的任何multiprocessing.Pool代码,但这将是一个更有希望开始的地方 - 假设你想要解决问题而不是想要确认你希望它必须是如果您只切换到较弱的API,则很容易做到; - )

使用multiprocessing继续进行“显而易见”的方法是使用Pool.apply_async()方法,将异步结果对象放在有界Queue.Queue上,并在主程序中包含线程离开Queue并等待结果出现。这很容易,但它并不神奇。它解决了您的问题,因为有界Queues 规范的方式,可以在不同速度的生产者和消费者之间进行调解。 concurrent.futures中的任何内容都没有直接解决 问题,而且它是“大量内存”问题的核心。

# Define global result_queue only in the main program.
import Queue
result_queue = Queue.Queue(100)  # pick a reasonable max size based on your problem

# Run this in as many threads as you like.
def consume_results():
    while True:
        a = result_queue.get()
        if a is None:
            break
        output(a.get())  # `output()` is your function

...
# main program passes out work, after starting threads
for i in range(1000):
    # the .put() will block so long as the queue is at its max size
    result_queue.put(pool.apply_async(calculate, args=(i,)))
# add sentinels to let threads know they're done
for i in range(number_of_threads_you_started):
    result_queue.put(None)

这就是你需要让生产者和消费者大致保持平衡的东西,并且任何标准库中都没有任何东西可以通过魔法为你做到这一点。

编辑 - 充实它

这是一个完整的可执行示例,任何拥有Python3的人都可以运行。注意:

  • 它不使用您的代码片段,因为它们依赖于外部数据库模块而不是每个人都可以运行。
  • 坚持concurrent.futures来管理进程和线程。使用multiprocessingthreading并不是很难,事实上 way 线程在这里使用,直接使用threading会更容易一些。但这种方式很清楚。
  • concurrent.futures Future对象与multiprocessing异步结果对象基本相同 - API功能的拼写方式不同。
  • 您的问题并不简单,因为它有多个阶段可以以不同的速度运行。同样,任何标准库中都没有任何东西可以通过魔法掩盖其潜在的不良后果。创建自己的有界队列仍然是最佳解决方案。对于MAX_QUEUE_SIZE
  • 的任何合理值,此处的内存使用率仍然适中
  • 通常不希望创建更多CPU绑定的工作进程,而不是比可用的核心数少一个。主程序还需要循环运行,操作系统也是如此。
  • 一旦你习惯了这个东西,这段代码中的所有注释都会很烦人,就像在代码行i += 1上看到注释“递增1”一样; - )

以下是代码:

import concurrent.futures as cf
import threading
import queue

NUM_CPUS = 3
NUM_THREADS = 4
MAX_QUEUE_SIZE = 20

# Runs in worker processes.
def producer(i):
    return i + 10

def consumer(i):
    global total
    # We need to protect this with a lock because
    # multiple threads in the main program can
    # execute this function simultaneously.
    with sumlock:
        total += i

# Runs in threads in main program.
def consume_results(q):
    while True:
        future = q.get()
        if future is None:
            break
        else:
            consumer(future.result())

if __name__ == "__main__":
    sumlock = threading.Lock()
    result_queue = queue.Queue(MAX_QUEUE_SIZE)
    total = 0
    NUM_TO_DO = 1000
    with cf.ThreadPoolExecutor(NUM_THREADS) as tp:
        # start the threads running `consume_results`
        for _ in range(NUM_THREADS):
            tp.submit(consume_results, result_queue)
        # start the worker processes
        with cf.ProcessPoolExecutor(NUM_CPUS) as pp:
            for i in range(NUM_TO_DO):
                # blocks until the queue size <= MAX_QUEUE_SIZE
                result_queue.put(pp.submit(producer, i))
        # tell threads we're done
        for _ in range(NUM_THREADS):
            result_queue.put(None)
    print("got", total, "expected", (10 + NUM_TO_DO + 9) * NUM_TO_DO // 2)

如果一切顺利,这是预期的输出:

got 509500 expected 509500