從原理到實(shí)戰(zhàn)詳解Python中文件并發(fā)讀寫的避坑指南
一、開篇直擊:為什么讀寫并發(fā)會讀到殘缺數(shù)據(jù)
“剛寫入的配置文件,讀取時(shí)居然一半是空的?”—— 這是 Linux 開發(fā)中高頻踩坑的場景。根源在于文件操作并非原子行為,當(dāng)寫進(jìn)程正在執(zhí)行磁盤 I/O 時(shí),讀進(jìn)程恰好介入,就會捕獲到文件的中間狀態(tài)。
舉個(gè)直觀例子:寫進(jìn)程要寫入 “Hello World”,實(shí)際會拆分為多次磁盤寫入操作。若讀進(jìn)程在 “Hello” 已寫入但 “ World” 未完成時(shí)觸發(fā)讀取,就會得到殘缺的 “Hello”。這種因執(zhí)行順序不確定導(dǎo)致的問題,在操作系統(tǒng)中被稱為競態(tài)條件(Race Condition) 。
更危險(xiǎn)的是,多進(jìn)程同時(shí)寫入時(shí),不僅會出現(xiàn)讀取殘缺,還可能導(dǎo)致數(shù)據(jù)覆蓋甚至文件損壞 —— 就像多人同時(shí)在同一頁紙上寫字,最終內(nèi)容必然混亂不堪。
二、三大解決方案:從簡單到復(fù)雜的選型指南
解決并發(fā)讀寫問題的核心是同步機(jī)制,即保證讀寫操作不會 “撞車”。以下三種方案覆蓋了不同場景需求,按推薦優(yōu)先級排序。
方案 1:臨時(shí)文件 + 原子替換(生產(chǎn)環(huán)境首選)
這是最穩(wěn)妥的 “笨辦法”,核心邏輯是 “先改副本,再換原件”,完美規(guī)避直接操作原文件的風(fēng)險(xiǎn)。
工作流程
- 創(chuàng)建臨時(shí)文件:寫進(jìn)程在同一目錄生成臨時(shí)文件(如
data.txt.tmp),避免直接修改data.txt; - 完整寫入數(shù)據(jù):將所有內(nèi)容寫入臨時(shí)文件,確保數(shù)據(jù)完全落盤;
- 原子替換文件:用
os.replace()(Linux 下底層調(diào)用rename系統(tǒng)調(diào)用)將臨時(shí)文件重命名為原文件。
為什么安全?
- 原子性保障:
rename操作在 ext4、XFS 等主流文件系統(tǒng)中是原子的 —— 要么完全替換,要么完全不替換,不存在中間狀態(tài); - 數(shù)據(jù)零損壞:即使寫進(jìn)程中途崩潰,原文件仍完好,僅殘留臨時(shí)文件可手動清理;
- 兼容性良好:無需依賴復(fù)雜鎖機(jī)制,所有 Linux 發(fā)行版均支持。
方案 2:文件鎖(復(fù)雜場景必備)
當(dāng)文件過大(復(fù)制臨時(shí)文件成本高)或需頻繁更新時(shí),文件鎖是更優(yōu)選擇。它通過 “鎖定 - 操作 - 釋放” 的流程,控制對文件的并發(fā)訪問。
常見鎖類型對比
| 鎖類型 | 特點(diǎn) | 適用場景 |
|---|---|---|
| 共享鎖(讀鎖) | 多進(jìn)程可同時(shí)持有,阻止寫操作 | 多進(jìn)程并發(fā)讀取 ?? |
| 排他鎖(寫鎖) | 僅單進(jìn)程持有,阻止所有讀寫操作 | 單進(jìn)程寫入或更新 ?? |
Python 實(shí)現(xiàn)推薦
原生fcntl模塊僅支持 Linux,跨平臺場景建議用filelock庫:
- 支持跨進(jìn)程同步(優(yōu)于
threading.Lock的線程級限制); - 自動管理鎖生命周期,避免手動清理鎖文件的疏漏;
- 可設(shè)置超時(shí)時(shí)間,防止死鎖風(fēng)險(xiǎn) 。
方案 3:數(shù)據(jù)庫替代(結(jié)構(gòu)化數(shù)據(jù)最優(yōu)解)
若文件存儲的是用戶信息、配置項(xiàng)等結(jié)構(gòu)化數(shù)據(jù),直接用數(shù)據(jù)庫(如 SQLite、Redis)是 “偷懶卻高效” 的選擇。數(shù)據(jù)庫內(nèi)置了完善的:
- 事務(wù)機(jī)制:保證 “讀 - 改 - 寫” 全流程原子性;
- 讀寫鎖:默認(rèn)支持多讀單寫的并發(fā)控制;
- 崩潰恢復(fù):即使進(jìn)程異常退出,也能通過日志回滾數(shù)據(jù)。
這種方式將并發(fā)控制交給專業(yè)工具,大幅減少業(yè)務(wù)代碼復(fù)雜度。
三、Python 實(shí)戰(zhàn):兩種核心方案代碼實(shí)現(xiàn)
實(shí)戰(zhàn) 1:臨時(shí)文件 + 原子替換(最通用)
結(jié)合tempfile模塊實(shí)現(xiàn)臨時(shí)文件自動管理,避免手動清理殘留文件:
import os
import tempfile
import time
def safe_write(filepath, content):
"""安全寫入文件:臨時(shí)文件+原子替換 ???"""
# 創(chuàng)建自動清理的臨時(shí)文件(suffix指定后綴,dir確保同文件系統(tǒng))
with tempfile.NamedTemporaryFile(
suffix=".tmp",
dir=os.path.dirname(filepath),
delete=False, # 手動控制刪除時(shí)機(jī) ?
mode="w",
encoding="utf-8"
) as tmp_f:
# 模擬耗時(shí)寫入過程
for char in content:
tmp_f.write(char)
tmp_f.flush() # 強(qiáng)制刷入磁盤 ??
print(f"[寫進(jìn)程] 已寫入: {tmp_f.tell()}/{len(content)}", end="\r")
time.sleep(0.1)
try:
# 確保數(shù)據(jù)完全落盤(避免內(nèi)存緩存導(dǎo)致的替換異常)
with open(tmp_f.name, "r") as f:
os.fsync(f.fileno())
# 原子替換原文件 ??
os.replace(tmp_f.name, filepath)
print(f"\n[寫進(jìn)程] 替換完成,{filepath} 已更新 ?")
except Exception as e:
# 異常時(shí)清理臨時(shí)文件 ??
os.unlink(tmp_f.name)
raise RuntimeError(f"寫入失敗: {e} ?")
# 測試:同時(shí)運(yùn)行讀寫進(jìn)程
if __name__ == "__main__":
target = "data.txt"
# 初始化原文件
if not os.path.exists(target):
with open(target, "w") as f:
f.write("初始內(nèi)容")
print(f"[寫進(jìn)程] 已創(chuàng)建初始文件 {target} ??")
# 模擬寫入
safe_write(target, "這是完整的新內(nèi)容,不會被讀殘缺!")
搭配讀者進(jìn)程測試效果:
import time
def continuous_read(filepath):
"""持續(xù)讀取文件,觀察內(nèi)容變化 ??"""
print("=== 讀進(jìn)程啟動 ===")
try:
while True:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
print(f"[讀進(jìn)程] 內(nèi)容: {repr(content)}")
time.sleep(0.5) # 間隔0.5秒讀取一次 ??
except KeyboardInterrupt:
print("\n讀進(jìn)程退出 ??")
if __name__ == "__main__":
continuous_read("data.txt")
運(yùn)行效果:讀進(jìn)程始終顯示 “初始內(nèi)容” 或完整新內(nèi)容,絕不會出現(xiàn)殘缺片段。
實(shí)戰(zhàn) 2:filelock 實(shí)現(xiàn)文件鎖(并發(fā)更新場景)
安裝依賴:pip install filelock
from filelock import FileLock
import time
def write_with_lock(filepath, content):
"""加排他鎖寫入文件 ??"""
# 鎖文件與目標(biāo)文件同目錄,便于管理
lock_path = f"{filepath}.lock"
with FileLock(lock_path, timeout=5): # 超時(shí)5秒避免死鎖 ??
print("[寫進(jìn)程] 獲得鎖,開始寫入 ??")
with open(filepath, "w", encoding="utf-8") as f:
for char in content:
f.write(char)
f.flush() # 強(qiáng)制刷盤 ??
time.sleep(0.1)
print("[寫進(jìn)程] 寫入完成,釋放鎖 ???")
def read_with_lock(filepath):
"""加共享鎖讀取文件 ??"""
lock_path = f"{filepath}.lock"
with FileLock(lock_path, shared=True, timeout=5): # 共享鎖允許多進(jìn)程讀 ??
print("[讀進(jìn)程] 獲得共享鎖,開始讀取 ??")
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
print(f"[讀進(jìn)程] 讀取內(nèi)容: {repr(content)}")
time.sleep(1) # 模擬讀取耗時(shí) ?
四、避坑指南:這些細(xì)節(jié)能省你 3 小時(shí)調(diào)試
原子替換的跨設(shè)備陷阱 :os.replace()僅在同一文件系統(tǒng)內(nèi)保證原子性。若臨時(shí)目錄(如/tmp)與目標(biāo)文件目錄在不同磁盤,會拋出AtomicMoveNotSupportedException。解決方案:用tempfile的dir參數(shù)指定臨時(shí)文件與目標(biāo)文件同目錄 。
鎖文件的殘留問題 :手動創(chuàng)建鎖文件易因進(jìn)程崩潰導(dǎo)致殘留,推薦用filelock庫 —— 它會在進(jìn)程退出時(shí)自動清理鎖文件,NFS 文件系統(tǒng)場景也能有效避免.nfs殘留文件 。
數(shù)據(jù)落盤的隱藏坑:即使write()調(diào)用成功,數(shù)據(jù)可能仍在內(nèi)存緩存中。務(wù)必用f.flush()+os.fsync(f.fileno())強(qiáng)制刷入磁盤,避免替換后讀取到緩存中的舊數(shù)據(jù) 。
Windows 與 Linux 的兼容性:跨平臺開發(fā)時(shí),fcntl模塊不可用,filelock是更好選擇;路徑分隔符用os.path模塊處理,避免硬編碼/導(dǎo)致 Windows 報(bào)錯。
五、總結(jié):按場景選對方案
| 場景 | 推薦方案 | 優(yōu)點(diǎn) |
|---|---|---|
| 配置文件更新、日志輪轉(zhuǎn) | 臨時(shí)文件 + 原子替換 | 簡單安全,無鎖依賴 |
| 大文件頻繁更新、多進(jìn)程讀寫 | 文件鎖(filelock) | 減少復(fù)制開銷,支持并發(fā)讀 |
| 結(jié)構(gòu)化數(shù)據(jù)存儲(用戶信息等) | 數(shù)據(jù)庫(SQLite/Redis) | 自帶事務(wù)與崩潰恢復(fù) |
文件并發(fā)讀寫的核心不是 “禁止并發(fā)”,而是 “有序并發(fā)” 。掌握臨時(shí)文件替換的簡單可靠,理解文件鎖的靈活控制,善用數(shù)據(jù)庫的專業(yè)能力 —— 這三種武器足以應(yīng)對 99% 的 Linux 文件操作場景 。
到此這篇關(guān)于從原理到實(shí)戰(zhàn)詳解Python中文件并發(fā)讀寫的避坑指南的文章就介紹到這了,更多相關(guān)Python文件并發(fā)讀寫內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python3實(shí)現(xiàn)的簡單三級菜單功能示例
這篇文章主要介紹了Python3實(shí)現(xiàn)的簡單三級菜單功能,涉及Python用戶交互以及針對json格式數(shù)據(jù)的遍歷、讀取、判斷等相關(guān)操作技巧,需要的朋友可以參考下2019-03-03
ChatGLM-6B+LangChain環(huán)境部署與使用實(shí)戰(zhàn)
這篇文章主要介紹了ChatGLM-6B+LangChain環(huán)境部署與使用方法,結(jié)合實(shí)例形式詳細(xì)分析了ChatGLM-6B+LangChain環(huán)境部署相關(guān)步驟、實(shí)現(xiàn)方法與相關(guān)注意事項(xiàng),需要的朋友可以參考下2023-07-07
如何基于Python實(shí)現(xiàn)一個(gè)慶祝國慶節(jié)的小程序
這篇文章主要介紹了如何基于Python實(shí)現(xiàn)一個(gè)慶祝國慶節(jié)的小程序,增加了互動選擇祝福語、查詢信息、播放背景音樂及趣味小測驗(yàn)等功能,使用tkinter增強(qiáng)GUI,提升用戶互動體驗(yàn),需要的朋友可以參考下2024-09-09
Python進(jìn)行ECB加密的實(shí)現(xiàn)流程
在信息安全領(lǐng)域,對稱加密算法AES因高效性和安全性被廣泛應(yīng)用,ECB作為AES最基礎(chǔ)的加密模式,因其簡單實(shí)現(xiàn)成為學(xué)習(xí)加密技術(shù)的入門案例,本文將系統(tǒng)解析ECB模式原理,并通過Python代碼演示完整加解密流程,需要的朋友可以參考下2025-09-09
pycharm 中mark directory as exclude的用法詳解
今天小編就為大家分享一篇pycharm 中mark directory as exclude的用法詳解,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-02-02
python+splinter實(shí)現(xiàn)12306網(wǎng)站刷票并自動購票流程
這篇文章主要為大家詳細(xì)介紹了python+splinter實(shí)現(xiàn)12306網(wǎng)站刷票并自動購票流程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09
一文帶你了解Python中的type,isinstance和issubclass
這篇文章主要為大家詳細(xì)介紹了Python中的type、isinstance和issubclass的使用,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-01-01
Python實(shí)現(xiàn)方便使用的級聯(lián)進(jìn)度信息實(shí)例
這篇文章主要介紹了Python實(shí)現(xiàn)方便使用的級聯(lián)進(jìn)度信息,實(shí)例分析了Python顯示級聯(lián)進(jìn)度信息的相關(guān)技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-05-05
python神經(jīng)網(wǎng)絡(luò)Keras實(shí)現(xiàn)GRU及其參數(shù)量
這篇文章主要為大家介紹了python神經(jīng)網(wǎng)絡(luò)Keras實(shí)現(xiàn)GRU及其參數(shù)量,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
探索Python中zoneinfo模塊處理時(shí)區(qū)操作實(shí)例
這篇文章主要為大家介紹了探索Python中zoneinfo模塊的用法實(shí)例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01

