Python多线程慎用shutil.make_archive打包

news2024/12/29 9:39:06

Python多线程慎用shutil.make_archive打包

记一下在工作中遇到一个错误,先说一下结论shutil.make_archive是线程不安全的,要慎重使用!!!

参考:https://stackoverflow.com/questions/41625702/is-shutil-make-archive-thread-safe

本篇文章会分别从多线程下使用shutil.make_archive打包会导致什么问题、原因是什么、如何解决三个方面进行讲解。

1 导致的问题

这里写个测试程序,按照规则创建一个文件夹,然后将这个文件夹打包成zip

def make_archive_use_shutil(f_number):
    p = f'test_archives/{f_number}'
    os.makedirs(p)
    print(f'archive: {f_number}.zip\n')
    shutil.make_archive(base_name=p, format='zip', root_dir=p)

单线程调用:

if __name__ == '__main__':
    shutil.rmtree('test_archives',ignore_errors=True)
    total = 1
    with ThreadPoolExecutor(max_workers=10) as executor:
        fs = [executor.submit(make_archive_use_shutil, i) for i in range(total)]
        for future in fs:
            future.result()

执行结果如下,是没有问题的

image-20230418094929057

但当我们把total改为10的时候再执行程序

if __name__ == '__main__':
    shutil.rmtree('test_archives',ignore_errors=True)
    total = 10
    with ThreadPoolExecutor(max_workers=10) as executor:
        fs = [executor.submit(make_archive_use_shutil, i) for i in range(total)]
        for future in fs:
            future.result()

问题出现了,报错:[Errno 2] No such file or directory: ‘test_archives/0’

image-20230418095312040

再看看压缩的文件夹,发现都乱套了。

image-20230418095424319

2 问题的原因

上面通过代码复现了多线程使用shutil.make_archive打包会导致的问题,接下来我们将从源码来分析一下导致问题的原因。

以下为Python3.9版本的make_archive方法实现

def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
                 dry_run=0, owner=None, group=None, logger=None):
    """Create an archive file (eg. zip or tar).

    'base_name' is the name of the file to create, minus any format-specific
    extension; 'format' is the archive format: one of "zip", "tar", "gztar",
    "bztar", or "xztar".  Or any other registered format.

    'root_dir' is a directory that will be the root directory of the
    archive; ie. we typically chdir into 'root_dir' before creating the
    archive.  'base_dir' is the directory where we start archiving from;
    ie. 'base_dir' will be the common prefix of all files and
    directories in the archive.  'root_dir' and 'base_dir' both default
    to the current directory.  Returns the name of the archive file.

    'owner' and 'group' are used when creating a tar archive. By default,
    uses the current owner and group.
    """
    sys.audit("shutil.make_archive", base_name, format, root_dir, base_dir)
    // 获取当前路径,并保存为临时变量
    save_cwd = os.getcwd()
    if root_dir is not None:
        if logger is not None:
            logger.debug("changing into '%s'", root_dir)
        base_name = os.path.abspath(base_name)
        if not dry_run:
          	// 切换路径
            os.chdir(root_dir)

    if base_dir is None:
        base_dir = os.curdir

    kwargs = {'dry_run': dry_run, 'logger': logger}

    try:
        format_info = _ARCHIVE_FORMATS[format]
    except KeyError:
        raise ValueError("unknown archive format '%s'" % format) from None

    func = format_info[0]
    for arg, val in format_info[1]:
        kwargs[arg] = val

    if format != 'zip':
        kwargs['owner'] = owner
        kwargs['group'] = group

    try:
        filename = func(base_name, base_dir, **kwargs)
    finally:
        if root_dir is not None:
            if logger is not None:
                logger.debug("changing back to '%s'", save_cwd)
            // 切换回原来保存的路径
            os.chdir(save_cwd)

    return filename

代码的大体思路是:

  1. 打包之前获取当前目录并保存变量save_cwd
  2. 切换目录到root_dir
  3. 执行打包逻辑
  4. 打包完成该切换回打包之前的save_cwd目录

单看逻辑是没有问题的,关键点在于,目录切换是进程级别的,也就是说,当一个进程中的一个线程切换目录之后,对另一个线程是可见的,另一个线程获取的当前目录也会随之改变,这就是问题的本质所在。

另外通过上面的报错,可以看出来最后打包的路径出现了嵌套的现象,是因为在打包过程中使用了相对路径,当多个线程进行目录切换的时候,相对路径也发生了变化。

3 解决方案

经过上面的分析,解决这个问题,就要避免线程之间不安全的目录切换,并且最好使用绝对路径代替原来的相对路径。

首先使用如下方法代替打包方法:

def make_archive_threadsafe(zip_name: str, path: str):
    with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zf:
        for root, dirs, files in os.walk(path):
            for file in files:
                zf.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), path))

将程序改造为使用绝对路径

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

def make_archive_use_custom(f_number):
    p = f'{BASE_DIR}/test_archives/{f_number}'
    os.makedirs(p)
    print(f'archive: {f_number}.zip\n')
    make_archive_threadsafe(f'{p}.zip', p)


if __name__ == '__main__':
    shutil.rmtree(f'{BASE_DIR}/test_archives',ignore_errors=True)
    total = 100
    with ThreadPoolExecutor(max_workers=20) as executor:
        fs = [executor.submit(make_archive_use_custom, i) for i in range(total)]
        for future in fs:
            future.result()

执行main方法测试之后打包是没有问题的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/442753.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++】从C语言入门C++的基础知识

C基础知识 前言1. C关键字2. 命名空间namespace命名空间的创建命名空间的使用命名空间的注意事项 3. C输入&输出4. 缺省参数概念分类全缺省参数半缺省参数 5. 函数重载概念实现C为什么能进行函数重载C和C的相互调用(可以不用看) 6. 引用概念注意事项…

相参积累

原理 在探测远距离目标时,由于目标回波信号比较微弱,信号幅度很小,从而导致接收信号的信噪比(SNR)过低,以至于信号处理算法检测不到目标,从而发生漏检。 在脉冲体制雷达中,雷达系统…

Oracle创建物化视图

Oracle创建物化视图 物化视图的语法物化视图的创建关于手动刷新创建手动刷新的物化视图 查看物化视图删除物化视图 物化视图的语法 物化视图的创建语法,如下所示: create materialized view [view_name] [ build immediate | build deferred ] [ refre…

LRU算法和LFU算法

LRU(Least Recently Used)最近最少使用,淘汰最近最少使用的数据, LFU(Least Frequently Used)最近最不频繁用,淘汰最不常用的数据。 LRU算法 传统的LRU基于链表实现。基本的一个LRU算法实现就…

Opencv 基本操作八 不均匀光照下的图像二值化探讨

在进行图像二值化时总是存在一些明部、暗部的干扰,单一的使用opencv提供的原始二值化方法很难做到预期效果。一般我们都会采用分块二值化(将图像切为多个局部进行二值化)、对比度提升(对值域进行线性或者非线性变换、直方图均衡化…

C#串口通信从入门到精通(2)——串口相关参数介绍

1、端口号(Port) 我们使用一个串口的时候,首先是要打开这个串口,那么我们怎么知道电脑上现在支持几个串口呢?对应的端口号又是什么呢? 由于我的电脑系统是window11,下面就以window11为例介绍如…

网络请求实战-缓存、缓存清理和HTTP缓存

目录 缓存介绍 清空策略(FIFO) 实战:fifo的memory函数 实战:LRU算法 HTTP缓存 Cache-Control 强制缓存 协商缓存 协商缓存-2(用的最多的) 小结 缓存介绍 早期cpu,内存设计上都有缓存…

开发常用的 Linux 命令4(系统、进程和其它)

开发常用的 Linux 命令4(系统、进程和其它) 作为开发者,Linux是我们必须掌握的操作系统之一。因此,在编写代码和部署应用程序时,熟练使用Linux命令非常重要。这些常用命令不得不会,掌握这些命令&#xff0…

【JUC】volatile和JMM

【JUC】volatile和JMM 文章目录 【JUC】volatile和JMM1. volatile1.1 特点1.2 内存语义 2. 内存屏障2.1 分类2.2 什么叫保证有序性?2.3 内存屏障的4种插入策略 3. volatile特性3.1 保证可见性3.2 volatile读写过程3.3 没有原子性3.4 指令禁重排(有序性) 4. 正确使用…

python标识符概念及规范

在python中 能取名字的东西非常非常多 例如 我们之前学的变量 以及后面要接触的 函数 类,等等,等等 而我们给这些取的名字 被统称为 标识符 而 python中 标识符的命名也是有限制的 主要有三种 1 内容限定 2 大小写铭感 3 不能使用关键字 内容限定来讲…

leetcode6_N字形变换

如有错误,感谢不吝赐教、交流 leetcode6 题目描述 将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。 比如输入字符串为 “PAYPALISHIRING” 行数为 3 时,排列如下: P A H N A P L S I…

HTB-SecNotes

HTB-SecNotes 信息收集8808端口80端口通过CSRF获取通过二次注入 立足tyler -> administrator 信息收集 8808端口 Windows IIS 10.0 可以从官方文档查看10.0版本可能的操作系统。 80端口 通过CSRF获取 目录扫描发现需要登陆后继续进一步操作啊。 对其进行简单的SQL注入测…

数据库基础篇 《7.单行函数》

目录 1. 函数的理解 1.1 什么是函数 1.2 不同DBMS函数的差异 ​编辑1.3 MySQL的内置函数及分类 ​编辑 2. 数值函数 2.1 基本函数 ​编辑 2.2 角度与弧度互换函数 2.3 三角函数 ​编辑 2.4 指数与对数 ​编辑 2.5 进制间的转换 ​编辑3. 字符串函数 ​编辑…

SAM(segment anything model)分割一切 Demo测试及API调用

SAM 分割一切 一,SAM介绍1.1 介绍1.2 项目链接 二,Demo-Test:2.1 Demo功能介绍2.1.1,首页就是这个SAM,点击try demo,可以选择它的自带图片,也可以自己添加。2.1.2 , 自己上传图片测试&#xff1…

[java基础]面向对象(五)

访问控制修饰符:--------------保护数据的安全(隐藏数据、暴露行为),实现封装 public:公开的,任何类 private:私有的,本类 protected:受保护的,本类、派生类、同包类 默认的&…

learn_C_deep_3 (最名不符实的关键字 - static、static关键字总结、基本数据类型、最冤枉的关键字 - sizeof)

目录 最名不符实的关键字 - static stati修饰全局变量和函数 static修饰局部变量 static关键字总结 几个问题 1.c语言要设置全局变量和函数可以跨文件使用的原因 2.C程序地址空间是什么样的? 3.局部变量为什么具有临时性 4.全局变量为什么具有全局性 5.为…

vue-cli版本号始终是2.9.6,且无法删除,安装更新无效的问题。

参考博客 目录 1.问题出现原因2.我的解决办法:删除原脚手架&删除原vuevue.cmd 1.问题出现原因 从各种博客我得知,这种问题出现在2处: 没有卸载原来的脚手架原来的vue和vue.cmd没删除干净 2.我的解决办法:删除原脚手架&…

[oeasy]python0135_命名惯用法_name_convention

命名惯用法 回忆上次内容 上次 了解了isidentifier的细节 关于 关键字关于 下划线 如何查询 变量所指向的地址? id 如何查询 已有的各种变量? locals 如果 用一个变量a的值 给另一个变量b 赋值是什么样的过程 呢??🤔…

当,Kotlin Flow与Channel相逢

前言 之前的文章已经分析了Flow的相关原理与简单使用,Flow之所以用起来香,Flow便捷的操作符功不可没,而想要熟练使用更复杂的操作符,那么需要厘清Flow和Channel的关系。 本篇文章构成: 1. Flow与Channel 对比 1.1 Fl…