Python中的异步持久性

时间:2019-05-03 02:24:21

标签: python performance parallel-processing multiprocessing dask

我需要加快Python脚本的执行速度,该Python脚本会分块读取一个较大的CSV文件,进行一些处理,然后将处理后的行保存到数据库中。处理10,000行然后保留它们需要花费相当的时间(1.5sec)。时间确实会有一些波动,当然,有时处理速度更快,有时会持续。

不幸的是,处理记录不容易并行化,因为处理是历史性的(记录是股票交易,并且基于先前的活动进行计算)。可能但对于这个问题,可以做的事情是并行处理一个块,并保留前一个块的结果。这应该使总时间减半。

for chunk in pd.read_csv(filename, chunksize=chunksize):
    # the following two tasks in parallel
    persist (rows_from_previous_chunk) # this is I/O waiting, mostly
    rows_to_save = process(chunk)      # this is Python, not C
    # wait for the above to finish
    rows_from_previous_chunk = rows_to_save

我的问题是,建议采用哪种方法进行上述操作。我可以想到一些:

  1. 鉴于一项任务主要是等待I / O,因此我有可能在不遇到GIL争用的情况下使用多线程。

  2. 第二种选择是使用Dask,特别是Delayed。但是,鉴于每个任务使用的时间都很短(不到2秒),因此我不确定这是最好的方法。

  3. 第三个选择是让一个进程读取和处理行,然后通过有界队列将它们发送到单独的行,该行将保存到数据库。我想到的是multiprocessing.Queue()

任何建议都值得赞赏。我是一个长期的Java程序员,最近改用Python并学习与GIL一起生活,因此是一个问题。

3 个答案:

答案 0 :(得分:1)

Dask确实增加了开销,但是与典型的2s任务时间相比,它很小。为了保持顺序,您可以让每个任务都依赖于前一个任务。这是一个刺

@dask.delayed
def process_save(rows_from_previous_chunk, chunk):
    if rows_from_previous_chunk:
        persist(rows_from_previous_chunk)
    return process(chunk)

parts = dd.read_csv(filename, chunksize=chunksize).to_delayed()

prev = None
for chunk in parts:
    prev = process_save(prev, chunk)
out = dask.delayed(persist)(prev)
dask.compute(out)

out.visualize()  # should look interesting

答案 1 :(得分:0)

这可能取决于您的数据库,但是如果存在,最简单的方法可能是使用诸如aiomysqlasyncpg之类的异步库来允许您在后台执行插入查询。

I / O绑定部分可以执行而无需GIL锁定,因此您的Python代码部分将能够继续。

答案 2 :(得分:0)

我最终采用了以下方法。有趣的是,使用多线程无法按预期工作。将数据帧传递到另一个队列进行保存仍在阻止主线程继续工作。并非100%知道发生了什么,但是为了节省时间,我转而使用流程,它可以正常工作。为了简化下面的代码,此代码有些简化,实际上我使用了多个db worker进程。

import multiprocessing

# this function will run into a separate process, saving the df asynchronously
def save(queue):
    db_engine = create_engine(...)
    while True:
        df  = queue.get()
        if df is None:
            break
        df.to_sql(schema="...", name="...", con=db_engine, if_exists="append", chunksize=1000, index=False)
        queue.task_done()

if __name__ == '__main__':

    queue = multiprocessing.JoinableQueue(maxsize=2) 
    worker = multiprocessing.Process(name="db_worker", target=save, args=(queue,))
    worker.daemon = True
    workers.start()

    # inside the main loop to process the df
        queue.put(df_to_save)

    # at the end 
    worker.join()  # wait for the last save job to finish before terminating the main process
相关问题