【问题标题】:Problem with Multiprocessing and Deadlocking in Python3Python3中的多处理和死锁问题
【发布时间】:2021-02-24 16:46:51
【问题描述】:

我的多处理有问题,我担心这是一个相当简单的解决方法,我只是没有正确地实现多处理。我一直在研究可能导致问题的事情,但我真正发现的是人们建议使用队列来防止这种情况,但这似乎并没有阻止它(再次,我可能只是在实施队列不正确)我已经在这几天了,我希望能得到一些帮助。 提前致谢!

import csv
import multiprocessing as mp
import os
import queue
import sys
import time

import connections
import packages
import profiles


def execute_extract(package, profiles, q):
    # This is the package execution for the extract
    # It fires fine and will print the starting message below
    started_at = time.monotonic()
    print(f"Starting {package.packageName}")
    try:
        oracle_connection = connections.getOracleConnection(profiles['oracle'], 1)
        engine = connections.getSQLConnection(profiles['system'], 1)
        path = os.path.join(os.getcwd(), 'csv_data', package.packageName + '.csv')
        cursor = oracle_connection.cursor()

        if os.path.exists(path):
            os.remove(path)

        f = open(path, 'w')
        chunksize = 100000
        offset = 0
        row_total = 0

        csv_writer = csv.writer(f, delimiter='^', lineterminator='\n')
        # I am having to do some data cleansing.  I know this is not the most efficient way to do this, but currently
        # it is what I am limited too 
        while True:
            cursor.execute(package.query + f'\r\n OFFSET {offset} ROWS\r\n FETCH NEXT {chunksize} ROWS ONLY')
            test = cursor.fetchone()
            if test is None:
                break
            else:
                while True:
                    row = cursor.fetchone()
                    if row is None:
                        break
                    else:
                        new_row = list(row)
                        new_row.append(package.sourceId[0])
                        new_row.append('')
                        i = 0
                        for item in new_row:
                            if type(item) == float:
                                new_row[i] = int(item)
                            elif type(item) == str:
                                new_row[i] = item.encode('ascii', 'replace')
                            i += 1
                        row = tuple(new_row)
                        csv_writer.writerow(row)
                        row_total += 1

            offset += chunksize

        f.close()
        # I know that execution is at least reaching this point.  I can watch the CSV files grow as more and more 
        # rows are added to the for all the packages What I never get are either the success message or error message
        # below, and there are never any entries placed in the tables 
        query = f"BULK INSERT {profiles['system'].database.split('_')[0]}_{profiles['system'].database.split('_')[1]}_test_{profiles['system'].database.split('_')[2]}.{package.destTable} FROM \"{path}\" WITH (FIELDTERMINATOR='^', ROWTERMINATOR='\\n');"
        engine.cursor().execute(query)
        engine.commit()

        end_time = time.monotonic() - started_at
        print(
            f"{package.packageName} has completed.  Total rows inserted: {row_total}.  Total execution time: {end_time} seconds\n")
        os.remove(path)
    except Exception as e:

        print(f'An error has occured for package {package.packageName}.\r\n {repr(e)}')

    finally:
        # Here is where I am trying to add an item to the queue so the get method in the main def will pick it up and
        # remove it from the queue 
        q.put(f'{package.packageName} has completed')
        if oracle_connection:
            oracle_connection.close()
        if engine:
            engine.cursor().close()
            engine.close()


if __name__ == '__main__':
    # Setting mp creation type
    ctx = mp.get_context('spawn')
    q = ctx.Queue()

    # For the Etl I generate a list of class objects that hold relevant information profs contains a list of 
    # connection objects (credentials, connection strings, etc) packages contains the information to run the extract 
    # (destination tables, query string, package name for logging, etc) 
    profs = profiles.get_conn_vars(sys.argv[1])
    packages = packages.get_etl_packages(profs)

    processes = []
    # I'm trying to track both individual package execution time and overall time so I can get an estimate on rows 
    # per second 
    start_time = time.monotonic()

    sqlConn = connections.getSQLConnection(profs['system'])
    # Here I'm executing a SQL command to truncate all my staging tables to ensure they are empty and will not 
    # generate any key violations 
    sqlConn.execute(
        f"USE[{profs['system'].database.split('_')[0]}_{profs['system'].database.split('_')[1]}_test_{profs['system'].database.split('_')[2]}]\r\nExec Sp_msforeachtable @command1='Truncate Table ?',@whereand='and Schema_Id=Schema_id(''my_schema'')'")

    # Here is where I start generating a process per package to try and get all packages to run simultaneously
    for package in packages:
        p = ctx.Process(target=execute_extract, args=(package, profs, q,))
        processes.append(p)
        p.start()

    # Here is my attempt at managing the queue.  This is a monstrosity of fixes I've tried to get this to work
    results = []
    while True:
        try:
            result = q.get(False, 0.01)
            results.append(result)
        except queue.Empty:
            pass
        allExited = True
        for t in processes:
            if t.exitcode is None:
                allExited = False
                break
        if allExited & q.empty():
            break

    for p in processes:
        p.join()

    # Closing out the end time and writing the overall execution time in minutes.
    end_time = time.monotonic() - start_time
    print(f'Total execution time of {end_time / 60} minutes.')

【问题讨论】:

  • 我提供的答案可以消除死锁或至少消除队列管理问题。不幸的是,有很多代码没有显示,即正在导入的包/模块。请记住,当在 Windows 等使用 spawn 而不是 fork 调用创建新进程的平台上运行时,您在全局范围内拥有的任何可执行代码都将由每个子进程执行。
  • @Booboo 队列管理在我看来并不是这里的问题。我可能做了不同的事情,但它应该可以工作(并且确实适用于玩具示例)。我会指出任何像你提到的那样在启动时做坏事的模块。
  • @Aaron 你和 JasonWH 有什么联系?你似乎在替他回答。
  • @Booboo 没有关系,我只是按照代码的逻辑,看不到队列管理如何死锁然后同意你关于imported 模块的说法......
  • @Aaron 感谢并再次感谢有关队列的信息。不知怎的,我错过了关于由于多线程/多处理语义,这不可靠的部分。但我承认这是一个相当模糊的陈述(不过我确实理解,不可靠) .如果q.empty() 不可靠而queue.Empty 异常可能不可靠(文档没有 说明这一点),您会认为文档会告诉您如何可靠地 阅读排队。

标签: python python-3.x multiprocessing etl


【解决方案1】:

我无法确定您遇到死锁的原因(我完全不相信这与您的队列管理有关),但我可以肯定地说,如果您执行以下操作之一,您可以简化队列管理逻辑两件事:

方法一

确保您的工作函数 execute_extract 即使在出现异常的情况下也会在结果队列中放置一些内容(我建议放置 Exception 对象本身)。然后,以while True: 开头并尝试获取结果的整个主流程循环可以替换为:

results = [q.get() for _ in range(len(processes))]

保证队列中的消息数量与创建的进程数相同。

方法2(更简单)

只需颠倒您等待子流程完成并处理结果队列的顺序。您不知道队列中有多少消息,但在所有进程都返回之前您不会处理队列。因此,无论队列中有多少消息,您都将得到。只需检索它们,直到队列为空:

for p in processes:
    p.join()
results = []
while not q.empty():
    results.append(q.get())

此时,我通常建议您使用多处理池类,例如 multiprocessing.Pool,它不需要显式队列来检索结果。但是进行这些更改中的任何一个(我建议使用方法 2,因为我看不到它如何导致死锁,因为此时只有主进程正在运行),看看你的问题是否消失了。但是,我不是,保证您的问题不在您的代码中的其他地方。虽然您的代码过于复杂且效率低下,但它显然不是“错误的”。至少你会知道你的问题是否在其他地方。

我的问题是:使用ctx = mp.get_context('spawn') 获取的上下文而不是仅调用multiprocessing 模块本身的方法对你有什​​么好处?如果您的平台支持fork 调用,这将是默认上下文,您不想使用它吗?

【讨论】:

  • 如果Queue 的大小受到限制或管道的缓冲区已满,则方法 2 可能会死锁。在joining 孩子之前尝试get 总是好的。我只永远依赖已知的确切数量的项目来“获取”或已知数量的“关闭”信号等于子进程的数量。 q.empty() 不可靠。
  • 当没有任何进程写入它并且只有一个进程检查它是否为空时,为什么q.empty() 在这种特殊情况下不可靠?我不是在怀疑你,我只是要求参考一些东西来解释为什么会这样。
  • 即使所有孩子都加入了,也需要一个单独的线程来反序列化对象,这使得empty 不能完全同步。来自文档:"empty() Return True if the queue is empty, False otherwise. Because of multithreading/multiprocessing semantics, this is not reliable."
  • 关于你的最后一段,spawnfork 更线程安全,如果有任何东西使用logging,这一点尤其重要。由于这个原因,python 的创建者正在慢慢地将 spawn 推为默认设置。也可能是因为windows只支持spawn,他们希望默认功能尽可能跨平台相同。
  • @Aaron 如果不测试q.empty() 而只是在循环中执行q.get_nowait() 直到得到queue.Empty 异常,该怎么办?那可靠吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多