导读:本期聚焦于小伙伴创作的《Python并发文件写入为什么需要加锁?锁与同步机制怎么用?》,敬请观看详情。多个线程同时往同一个文件里写内容,常常出现数据错乱、内容覆盖或者换行黏连的问题。根本原因是文件对象在底层并不是线程安全的,操作系统调度线程时会随机切换,导致一次写操作被拆成多段。用threading模块的Lock可以让临界区串行执行,也可以用队列加单线程消费者来彻底隔离写入。本文从底层调度讲起,对比互斥锁、可重入锁和队列模型的适用场景,并给出可直接运行的代码示例,帮你避开并发写文件的常见坑。

在Python多线程程序中,如果多个工作线程共同向同一个日志文件或数据文件执行写入,往往会得到残缺不全的结果。这并不是Python本身有bug,而是因为标准库里的文件对象没有内置线程安全保护,当操作系统在两个线程的写调用之间发生上下文切换,字节流就会互相穿插。

Python并发文件写入为什么需要加锁?锁与同步机制怎么用?

一、为什么并发写文件会出问题

CPython的文件写操作通常分为两步:先把用户缓冲区的数据准备好,再调用系统调用write落到内核。线程A刚把一半内容送进缓冲区,时间片用完,线程B抢到CPU并把另一段内容写入,最终文件里两行日志交错在一起。更隐蔽的情况是,某些平台上的write并不保证原子性,尤其是写入长度超过PIPE_BUF或者使用了带缓冲的TextIOWrapper时。

下面这段示例代码模拟了两个线程不加保护地写同一个文件,运行几次就能看到内容混乱:

import threading

def bad_writer(name, f):
    for i in range(100):
        f.write(f"线程{name}写入第{i}行n")

with open("test_bad.log", "w", encoding="utf-8") as f:
    t1 = threading.Thread(target=bad_writer, args=(1, f))
    t2 = threading.Thread(target=bad_writer, args=(2, f))
    t1.start()
    t2.start()
    t1.join()
    t2.join()

上述代码里两个线程共用同一个文件句柄f,没有任何同步,最终test_bad.log里经常出现“线程1写入第线程2写入第”这样的夹杂行。这种问题在压力小的时候可能很久才出现一次,一旦上线就很难排查。

二、使用Lock互斥锁做同步

threading.Lock是最直接的方案。它只有两个状态:锁定和未锁定。哪个线程先调用acquire拿到锁,其他线程就必须阻塞等待,直到release释放。把写文件的逻辑放进锁保护的临界区,就能保证同一时刻只有一个线程在写。

改写后的安全版本如下:

import threading

lock = threading.Lock()

def safe_writer(name, f):
    for i in range(100):
        with lock:
            f.write(f"线程{name}写入第{i}行n")
            f.flush()

with open("test_safe.log", "w", encoding="utf-8") as f:
    t1 = threading.Thread(target=safe_writer, args=(1, f))
    t2 = threading.Thread(target=safe_writer, args=(2, f))
    t1.start()
    t2.start()
    t1.join()
    t2.join()

这里用with lock自动管理锁的获取和释放,即使写的过程中抛异常也不会死锁。flush确保数据及时刷盘,避免进程退出时缓冲丢失。Lock的优点是简单直观,缺点是所有写操作串行,高并发下可能成为瓶颈。

如果同一个线程可能嵌套调用写函数,应该用threading.RLock代替Lock。RLock允许持有锁的线程重复acquire,每加一次锁计数加一,release对应减一,直到计数归零才真正释放,可避免自己锁死自己。

三、用队列解耦生产与消费

另一种更优雅的思路是:工作线程只往queue.Queue里塞数据,单独启动一个writer线程从队列取数据并写文件。这样文件句柄只被一个线程接触,从根本上消灭竞争,也不用给每次写加锁。

import threading
import queue

q = queue.Queue()

def producer(name):
    for i in range(100):
        q.put(f"线程{name}写入第{i}行n")

def consumer(f):
    while True:
        line = q.get()
        if line is None:
            break
        f.write(line)
        f.flush()
        q.task_done()

with open("test_queue.log", "w", encoding="utf-8") as f:
    c = threading.Thread(target=consumer, args=(f,))
    c.start()
    p1 = threading.Thread(target=producer, args=(1,))
    p2 = threading.Thread(target=producer, args=(2,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    q.put(None)
    c.join()

队列模型把“产生日志”和“落盘”分开,生产者之间互不影响,消费者单线程顺序写,既安全又容易扩展成多文件或者网络发送。代价是多了一个常驻线程和一层内存缓冲,需要处理好程序退出时的收尾,比如放一个None作为结束信号。

四、方案对比与选择建议

三种常见做法的特点可以归纳如下:

方案实现复杂度性能表现适用场景
无锁共用句柄最低看似最快实则数据错乱绝不推荐
Lock/RLock保护写操作串行,中等吞吐线程少、偶尔写
Queue单线程写生产者并行,吞吐高高频日志、多生产者

实际项目里,如果只是在几个线程里偶尔记点状态,加把Lock足够。如果是Web服务里几十个线程打日志,用队列或者直接用logging模块的内置QueueHandler更稳妥。无论选哪种,记住文件对象本身不帮你想好同步,责任永远在写代码的人身上。

五、小结

并发文件写入的核心矛盾是“共享资源加非原子操作”。理解线程切换导致的交错,才能明白锁不是多余的动作。Lock解决有无问题,RLock解决重入问题,Queue解决架构隔离问题。把示例跑一遍,观察加锁前后文件内容的差异,比单纯记结论更有用。

Python文件写入线程同步修改时间:2026-08-10 12:09:47

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。