【问题标题】:Does copying a dict to multiple processes also copy the objects in the dict?将字典复制到多个进程是否也会复制字典中的对象?
【发布时间】:2019-08-29 10:48:17
【问题描述】:

我正在多个进程上运行一个函数,该函数将大熊猫数据帧的字典作为输入。启动进程时,将字典复制到每个进程,但据我了解,字典仅包含对数据帧的引用,因此数据帧本身不会复制到每个进程。这是正确的,还是每个进程都会获得字典的深层副本?

import numpy as np
from multiprocessing import Pool, Process, Manager

def process_dataframe(df_dict, task_queue, result_queue):
    while True:
        try:
            test_value = task_queue.get(block=False)
        except:
            break
        else:
            result = {df_name: df[df==test_value].sum() for df_name, df in df_dict.items()}
            result_queue.put(result)



if __name__ == '__main__':
    manager = Manager()
    task_queue = manager.Queue()
    result_queue = manager.Queue()

    df_dict = {'df_1': some_large_df1, 'df_2': some_large_df2, ...}
    test_values = np.random.rand(1000)

    for val in test_values:
        task_queue.put(val)

    with Pool(processes=4) as pool:
        processes = []
        for _ in range(4):

            # Is df_dict copied shallow or deep to each process?
            p = pool.Process(target=process_dataframe, args=(df_dict,task_queue,result_queue))  
            processes.append(p)
            p.start()

        for p in processes:
            p.join()

    results = [result_queue.get(block=False) for _ in range(result_queue.qsize())]

【问题讨论】:

  • 你在哪里复制它? df_dict 永远不会被复制。我看到它作为参数传递给 pool.Process... 因为它是作为参数传递的,所以整个 dict 作为指针传递。所以即使是浅拷贝也不会发生。
  • 出于这个原因,您还应该使您的dict 线程安全。
  • @Error-SyntacticalRemorse 我在某处读到,参数在发送到每个进程之前被腌制。那么只有指针会被腌制吗?

标签: python-3.x


【解决方案1】:

TLDR:它确实传递了一个足够有趣的副本。但不是以正常方式。子进程和父进程共享相同的内存,除非其中一个或另一个更改了对象(在实现写时复制的系统上[windows 和 linux 都有这个])。在这种情况下,会为已更改的对象分配内存。

我坚信,最好是亲眼所见,而不是仅仅被告知,让我们开始吧。

为此,我从网上提取了一些示例 multiprocessing 代码。示例代码符合回答此问题的要求,但与您问题中的代码不匹配。

以下所有代码都是一个脚本,但我将把它拆开来解释每一部分。


开始示例:

首先让我们创建一个dictionary。我将使用它而不是 DataFrame,因为它们的行为相似,但我不需要安装软件包即可使用它。

注意:id()语法,它返回一个对象的唯一标识

# importing the multiprocessing module 
import multiprocessing
import sys # So we can see the memory we are using 

myDict = dict()
print("My dict ID is:", id(myDict))
myList = [0 for _ in range(10000)] # Just a massive list of all 0s
print('My list size in bytes:', sys.getsizeof(myList))
myDict[0] = myList
print("My dict size with the list:", sys.getsizeof(myDict))
print("My dict ID is still:", id(myDict))
print("But if I copied my dic it would be:", id(myDict.copy()))

对我来说这是输出:

我的字典 ID 是:139687265270016
我的列表大小(以字节为单位):87624
我的字典大小与列表:240
我的字典 ID 仍然是:139687265270016
但如果我复制我的 dic,它将是:139687339197496

很酷,所以我们看到id 会在我们复制对象时发生变化,并且我们看到dictionary 只是持有一个指向list 的指针(因此dict 的内存大小要小得多)。

现在让我们看看Process 是否复制了字典。

def method1(var): 
    print("method1 dict id is:", str(id(var)))

def method2(var): 
    print("method2 dict id is:", str(id(var))) 

if __name__ == "__main__": 
    # creating processes 
    p1 = multiprocessing.Process(target=method2, args=(myDict, )) 
    p2 = multiprocessing.Process(target=method1, args=(myDict, )) 

    # starting process 1 
    p1.start() 
    # starting process 2 
    p2.start() 

    # wait until process 1 is finished 
    p1.join() 
    # wait until process 2 is finished 
    p2.join() 

    # both processes finished 
    print("Done!") 

在这里,我将myDict 作为arg 传递给我的两个子进程函数。 这是我得到的输出:

method2 dict id 为:139687265270016
method1 dict id 为:139687265270016
完成!

注意:id 与我们之前在代码中定义字典时相同。

这是什么意思?

如果id 永远不会改变,那么我们在所有实例中都使用相同的对象。所以理论上,如果我对Process 进行更改,它应该会更改主要对象。但这并没有像我们预期的那样发生。

例如:让我们更改我们的method1

def method1(var): 
    print("method1 dict id is:", str(id(var)))
    var[0][0] = 1
    print("The first five elements of the list in the dict are:", var[0][:5])

在我们的p2.join() 之后添加一对prints:

p2.join()
print("The first five elements of the list in the dict are:", myDict[0][:5])
print("The first five elements of the list are:", myList[:5])

我的字典 ID 是:140077406931128
我的列表大小(以字节为单位):87624
我的字典大小与列表:240
我的字典 ID 仍然是:140077406931128
但如果我复制我的 dic,它将是:140077455160376
method1 dict id 为:140077406931128
dict中列表的前五个元素是:[1, 0, 0, 0, 0]
method2 dict id 为:140077406931128
dict中列表的前五个元素是:[0, 0, 0, 0, 0]
列表的前五个元素是:[0, 0, 0, 0, 0]
完成!

这很有趣...ids 是一样的,我可以在函数中更改对象但dict 在主进程中不会更改...

经过进一步调查,我发现了这个问题/答案:https://stackoverflow.com/a/14750086/8150685

创建子进程时,子进程继承父进程的副本(包括ids! 的副本);但是,(如果您使用的操作系统实现了 COW(写时复制),则孩子和父母将使用相同的内存,除非孩子或父母对数据进行更改,在这种情况下,内存将仅分配给您更改的变量(在您的情况下,它将复制您更改的 DataFrame)。

很抱歉这篇文章太长了,但我认为工作流程会很好看。

希望这会有所帮助。如果对您有帮助,请不要忘记在https://stackoverflow.com/a/14750086/8150685 上投票并回答问题。

【讨论】:

  • @connesy 如果它解决了您的问题,请不要忘记将其标记为答案。
  • 非常感谢您的彻底回复,这很有启发性!非常感谢您的彻底回复,这很有启发性!我有几个后续问题: 1:如果我正确理解了您的示例,则将dictid 复制到子进程中,这意味着子进程中dictid 将与父进程中的id 相同,即使我在其中一个进程中对dict 进行了更改? 2:根据您链接的 q/a 上的 [this comment](bit.ly/2U8U5IX),DataFrames 可能通过阅读被复制?
  • 仅当您修改数据时。阅读不算修改。换句话说,如果您所做的只是读取数据帧,那么将不会使用额外的内存;但是,如果您进行更改,整个数据框将在后台复制。但是,数据框的 ID 仍然不会更改。 (ID 是该进程的本地 ID,因此从我的测试来看,即使我在单独的进程中更改了对象,它也没有改变)。
猜你喜欢
  • 1970-01-01
  • 2013-01-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-11
  • 2023-03-28
  • 2020-11-25
  • 1970-01-01
相关资源
最近更新 更多