วิธีทำ File Locking ใน Python ให้รองรับทั้ง Windows และ Unix

10 นาที 13 views บันทึกเป็น PDF
วิธีทำ File Locking ใน Python ให้รองรับทั้ง Windows และ Unix

เจอปัญหาเขียนไฟล์ทับกันจนพังใช่ไหม? มาดูวิธีทำระบบล็อกไฟล์ (File Locking) ด้วย Python ให้ใช้งานได้ทั้ง Windows และ Unix แบบไม่ต้องง้อไลบรารีภายนอกกัน

ทำไมโปรแกรมต้องแย่งกันเขียนไฟล์

ลองนึกภาพว่าคุณมีสมุดบันทึกเล่มหนึ่งวางอยู่บนโต๊ะ แล้วมีเพื่อนสองคนพยายามเขียนลงในสมุดพร้อมกัน ถ้าต่างคนต่างเขียนโดยไม่ดูว่าอีกคนกำลังเขียนอยู่ ข้อความก็จะทับซ้อนกันจนอ่านไม่รู้เรื่อง หรือสมุดอาจจะพังไปเลย ในโลกของโปรแกรมก็เกิดเหตุการณ์แบบนี้ได้เหมือนกัน เรียกว่า Concurrent writes (การเขียนข้อมูลพร้อมกันจากหลายที่) ซึ่งมักเกิดขึ้นเมื่อแอปพลิเคชันหน้าจอสวยๆ (GUI) กับโปรแกรมเบื้องหลัง (Background process) พยายามบันทึกข้อมูลลงไฟล์เดียวกันพร้อมกัน

ถ้าจังหวะไม่ดี ข้อมูลชุดหนึ่งจะไปทับอีกชุด ทำให้ไฟล์เสียหาย (File corruption) วิธีแก้ปัญหามาตรฐานคือการทำ File locking (การล็อกไฟล์เพื่อไม่ให้คนอื่นเข้าถึง) ซึ่งเป็นการบอกโปรแกรมอื่นว่า "ไฟล์นี้ฉันกำลังใช้งานอยู่ ห้ามใครมายุ่งจนกว่าฉันจะเสร็จ" ถ้าเราไม่ทำระบบล็อกไว้ โปรแกรมอาจเขียนข้อมูลครึ่งๆ กลางๆ จนไฟล์ตั้งค่าโปรแกรมใช้งานไม่ได้อีกเลย

ปัญหาคือการทำระบบล็อกไฟล์ในภาษา Python ให้ใช้ได้ทั้งบน Windows และระบบ Unix (เช่น Linux หรือ macOS) นั้นยากมาก เพราะสองระบบนี้ใช้ API (ช่องทางเชื่อมต่อระหว่างโปรแกรม) คนละตัวกันโดยสิ้นเชิง บทความนี้จะพาคุณไปดูวิธีสร้างระบบล็อกไฟล์แบบไม่ต้องพึ่งพาไลบรารีภายนอก (Third-party library) เพื่อให้โปรแกรมของคุณทำงานร่วมกันได้อย่างปลอดภัย

รู้จักกับ fcntl บนระบบ Unix

บนระบบปฏิบัติการตระกูล Unix เราจะใช้โมดูล fcntl ในการจัดการล็อกไฟล์ ฟังก์ชันที่สำคัญที่สุดคือ flock() ซึ่งเป็นการทำ Advisory lock (การล็อกแบบตกลงกันเอง) หมายความว่าระบบปฏิบัติการจะแค่บันทึกสถานะไว้ แต่จะไม่บังคับห้ามโปรแกรมอื่นเขียนทับ ถ้าโปรแกรมอื่น "ดื้อ" ไม่ยอมเช็กสถานะการล็อกก่อน มันก็สามารถเข้าไปเขียนไฟล์ได้อยู่ดี ดังนั้นโปรแกรมทุกตัวที่เกี่ยวข้องต้องยอมทำตามกฎเดียวกัน

การใช้งาน flock() มีจุดที่ต้องระวังคือถ้าเราสั่งล็อกโดยไม่ตั้งค่าอะไรเลย โปรแกรมจะหยุดนิ่ง (Block) เพื่อรอจนกว่าจะได้ล็อก ซึ่งถ้าเป็นแอปที่มีหน้าจอผู้ใช้ (GUI) จะทำให้หน้าจอค้างไปเลย วิธีแก้คือเราจะใช้ตัวเลือก LOCK_NB (Non-blocking) เพื่อบอกว่าถ้าล็อกไม่ได้ให้แจ้งเตือนทันที แทนที่จะยืนรอ

นี่คือตัวอย่างการใช้ fcntl ในการล็อกไฟล์:

import fcntl

# เปิดไฟล์ขึ้นมาเพื่อเตรียมล็อก
fd = open('target.lock', 'w')

try:
    # พยายามล็อกไฟล์แบบ exclusive (ห้ามคนอื่นเข้า) และแบบไม่รอ (LOCK_NB)
    fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
    print("ล็อกไฟล์สำเร็จ")
except OSError:
    # ถ้ามีคนอื่นล็อกอยู่แล้ว จะเกิด error ขึ้นที่นี่
    print("ไฟล์ถูกล็อกอยู่ เข้าถึงไม่ได้")

คำอธิบายโค้ด: บรรทัดแรกคือการนำเข้าโมดูลจัดการไฟล์ ต่อมาคือการเปิดไฟล์เป้าหมาย ส่วนคำสั่ง fcntl.flock คือหัวใจสำคัญที่ใช้ร้องขอสิทธิ์การเขียน หากทำสำเร็จโปรแกรมจะทำงานต่อได้ แต่ถ้ามีโปรแกรมอื่นล็อกอยู่แล้วจะกระโดดไปที่ except ทันที ผลลัพธ์ที่ได้คือข้อความแจ้งเตือนว่า "ล็อกไฟล์สำเร็จ" หรือ "ไฟล์ถูกล็อกอยู่" ตามสถานการณ์จริง

การล็อกไฟล์บน Windows ด้วย msvcrt

บน Windows เราไม่มีโมดูล fcntl แต่เรามี msvcrt แทน ฟังก์ชันที่ใช้คือ locking() ซึ่งมีลักษณะการทำงานต่างออกไปนิดหน่อย คือมันจะล็อกไฟล์เป็น Byte range (ช่วงของไบต์ในไฟล์) แทนที่จะล็อกทั้งไฟล์เหมือน Unix เพื่อความง่ายในการเขียนโปรแกรม เรามักจะล็อกแค่ 1 ไบต์แรกของไฟล์ก็เพียงพอแล้ว

ความแตกต่างที่สำคัญคือ msvcrt.locking() ต้องการหมายเลข File descriptor (ตัวเลขระบุไฟล์ของระบบ) ในการทำงาน เราจึงต้องส่งค่า fd.fileno() เข้าไปแทนที่จะส่งตัววัตถุไฟล์ตรงๆ เหมือนวิธีของ Unix และเรายังคงใช้ตัวเลือก LK_NBLCK เพื่อป้องกันไม่ให้โปรแกรมค้างหากมีการล็อกอยู่ก่อนแล้ว

ตัวอย่างการล็อกไฟล์บน Windows มีดังนี้:

import msvcrt

# เปิดไฟล์ในโหมดอ่านและเขียนแบบ binary
fd = open('target.lock', 'r+b')

try:
    # ล็อก 1 ไบต์แรกของไฟล์โดยไม่รอ
    msvcrt.locking(fd.fileno(), msvcrt.LK_NBLCK, 1)
    print("ล็อกไฟล์บน Windows สำเร็จ")
except OSError:
    print("ไฟล์ถูกล็อกอยู่บน Windows")

คำอธิบายโค้ด: เราเปิดไฟล์ด้วยโหมด r+b เพื่อให้รองรับการเขียนแบบไบนารี คำสั่ง fd.fileno() คือการดึงหมายเลขระบุตัวตนของไฟล์ออกมาให้ Windows รู้จัก ส่วนเลข 1 สุดท้ายคือจำนวนไบต์ที่จะล็อก ผลลัพธ์ที่ได้คือโปรแกรมจะพิมพ์ข้อความบอกสถานะล็อกตามการใช้งานจริงของระบบ

สร้างระบบ Cross-platform ด้วย sys.platform

เพื่อให้โปรแกรมรันได้ทุกที่ เราต้องเขียนโค้ดที่เลือกใช้โมดูลตามระบบปฏิบัติการโดยอัตโนมัติ เราจะใช้ sys.platform ในการตรวจสอบชื่อระบบ ถ้าเป็น win32 ให้เรียกใช้ msvcrt แต่ถ้าเป็นอย่างอื่นให้ใช้ fcntl แทน สิ่งสำคัญคือต้อง import โมดูลภายในเงื่อนไข if เท่านั้น

เหตุผลที่ต้องทำแบบนี้เพราะถ้าเราเขียน import fcntl ไว้บนสุดของไฟล์ บน Windows จะเกิดความผิดพลาดทันทีว่าหาโมดูลไม่เจอ การนำเข้าเฉพาะตอนที่จำเป็นจะช่วยให้โค้ดชุดเดียวกันรันได้ทั้งสองระบบโดยไม่มีข้อผิดพลาดเรื่องไลบรารีที่ไม่มีอยู่จริงในระบบนั้นๆ

โครงสร้างการเขียนโค้ดสำหรับทุกระบบเป็นดังนี้:

import sys

def lock_file(fd):
    if sys.platform == 'win32':
        import msvcrt
        msvcrt.locking(fd.fileno(), msvcrt.LK_NBLCK, 1)
    else:
        import fcntl
        fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)

คำอธิบายโค้ด: ฟังก์ชันนี้รับค่าไฟล์ที่เปิดแล้วเข้ามาตรวจสอบว่ารันอยู่บน Windows หรือไม่ หากใช่จะใช้ msvcrt ถ้าไม่ใช่จะใช้ fcntl วิธีนี้ทำให้เราไม่ต้องเขียนโค้ดแยกไฟล์ ผลลัพธ์คือฟังก์ชันนี้จะทำหน้าที่ล็อกไฟล์ให้เราไม่ว่าคุณจะรันบนเครื่องไหนก็ตาม

ใช้ไฟล์ Sidecar เพื่อความปลอดภัย

มือใหม่หลายคนมักจะเผลอไปล็อกไฟล์ข้อมูลจริงๆ เช่น sites.json ซึ่งเป็นวิธีที่ไม่แนะนำ เพราะถ้าโปรแกรมอื่นพยายามเปิดไฟล์นั้นเพื่ออ่านข้อมูล Windows อาจจะไม่อนุญาตให้เปิดเลยถ้ามีโปรแกรมหนึ่งล็อกไฟล์นั้นไว้ วิธีที่ดีกว่าคือการใช้ Sidecar file (ไฟล์ข้างเคียงที่สร้างมาเพื่อล็อกโดยเฉพาะ) เช่นไฟล์ .lock

หลักการคือเราจะสร้างไฟล์เปล่าๆ ขึ้นมาหนึ่งไฟล์เพื่อใช้ล็อกเท่านั้น ส่วนไฟล์ข้อมูลจริงๆ เราจะอ่านหรือเขียนปกติโดยไม่ต้องไปยุ่งกับมัน วิธีนี้ช่วยลดความเสี่ยงที่ระบบปฏิบัติการจะกั้นไม่ให้เราเข้าถึงข้อมูล และยังทำให้การจัดการล็อกง่ายขึ้นมาก เพราะเราไม่ต้องกังวลเรื่องการจัดการช่วงไบต์ในไฟล์ข้อมูลสำคัญ

เพื่อป้องกันไม่ให้สองโปรแกรมสร้างไฟล์ล็อกพร้อมกันจนเกิดความผิดพลาด เราจะใช้คำสั่ง os.open พร้อมแฟล็ก O_CREAT | O_EXCL ซึ่งเป็นการสั่งให้ระบบปฏิบัติการ "สร้างไฟล์ถ้าไม่มีอยู่ แต่ถ้ามีอยู่แล้วให้แจ้งเตือนทันที" ภายในขั้นตอนเดียว (Atomic operation) ทำให้ไม่มีช่องว่างให้โปรแกรมอื่นแทรกเข้ามาได้

การจัดการไฟล์ค้าง (Stale locks)

ปัญหาที่น่าปวดหัวที่สุดคือถ้าโปรแกรมเกิดค้างหรือพังไปดื้อๆ ขณะที่ยังล็อกไฟล์อยู่ ไฟล์ .lock จะยังคงอยู่บนเครื่องตลอดไป ทำให้ไม่มีใครสามารถเข้าถึงข้อมูลได้อีกเลย เราเรียกสิ่งนี้ว่า Stale lock (การล็อกที่ค้างอยู่โดยไม่มีคนถือ) วิธีแก้ที่นิยมคือการตรวจสอบเวลาการแก้ไขไฟล์ล่าสุด

ถ้าเราพบว่าไฟล์ล็อกมีอายุเก่าเกินไป เช่น นานกว่า 30 นาที เราสามารถอนุมานได้ว่าโปรแกรมเดิมน่าจะพังไปแล้ว เราจึงสั่งลบไฟล์นั้นทิ้งและเริ่มกระบวนการล็อกใหม่ วิธีนี้แม้จะไม่สมบูรณ์แบบ 100% แต่ก็เป็นวิธีปฏิบัติมาตรฐานที่ใช้กันในงานจริงเพื่อป้องกันโปรแกรมล็อกตัวเองตาย

ตัวอย่างการเช็กความเก่าของไฟล์:

import os, time

lock_file = 'target.lock'
if os.path.exists(lock_file):
    # เช็กเวลาแก้ไขล่าสุด
    mtime = os.path.getmtime(lock_file)
    if time.time() - mtime > 1800: # ถ้าเก่ากว่า 30 นาที
        os.remove(lock_file) # ลบไฟล์ทิ้ง
        print("ลบไฟล์ล็อกเก่าทิ้งแล้ว")

คำอธิบายโค้ด: เราใช้ os.path.getmtime เพื่อดูเวลาแก้ไขไฟล์ล่าสุด หากเวลาปัจจุบันห่างจากเวลาไฟล์เกิน 1800 วินาที แสดงว่าไฟล์นั้นเก่าเกินไป ผลลัพธ์คือไฟล์จะถูกลบออกเพื่อเปิดโอกาสให้โปรแกรมอื่นเข้ามาทำงานต่อได้ทันที

สรุป: นำไปใช้งานจริง

การทำระบบล็อกไฟล์อาจดูซับซ้อนในตอนแรก แต่เมื่อเข้าใจว่ามันคือการสื่อสารระหว่างโปรแกรมผ่าน "ไฟล์ตัวแทน" ทุกอย่างจะง่ายขึ้นมาก คุณควรนำเทคนิคการทำ Context manager (การใช้คำสั่ง with เพื่อจัดการเปิด-ปิดไฟล์อัตโนมัติ) มาใช้เพื่อให้โค้ดดูสะอาดและปลอดภัย

ตัวอย่างการนำไปใช้จริง: สมมติคุณทำโปรแกรมบันทึกข้อมูลผู้ใช้ลงไฟล์ config.json ในทุกๆ 5 นาที ก่อนจะเขียนไฟล์ให้คุณเปิดไฟล์ config.json.lock ขึ้นมาล็อกก่อนเสมอ เมื่อเขียนไฟล์ข้อมูลเสร็จแล้วให้ปิดไฟล์ล็อกนั้นทิ้ง วิธีนี้จะช่วยให้โปรแกรมของคุณไม่มีวันเขียนข้อมูลชนกันแม้จะรันหลายหน้าต่างพร้อมกัน เป็นพื้นฐานสำคัญที่โปรแกรมเมอร์มืออาชีพต้องมีติดตัวไว้ครับ


ที่มา: Cross-platform file locking in Python — fcntl vs msvcrt from scratch — DEV Community

แชร์บทความ

Facebook X LINE

บทความที่เกี่ยวข้อง

จัดการเซิร์ฟเวอร์ผ่าน VS Code ให้ง่ายขึ้นด้วย Easy SSH พร้อมฟีเจอร์โหลดไฟล์ผ่านคลิกเดียว

จัดการเซิร์ฟเวอร์ผ่าน VS Code ให้ง่ายขึ้นด้วย Easy SSH พร้อมฟีเจอร์โหลดไฟล์ผ่านคลิกเดียว

เบื่อไหมที่ต้องสลับหน้าจอไปมาเพื่อจัดการเซิร์ฟเวอร์? มาลองใช้ Easy SSH ปลั๊กอิน VS Code ที่ช่วยให้คุณรีโมทผ่าน Terminal ได้สะดวก แถมโหลดไฟล์ได้ง่ายแค่กด Ctrl+click

ที่มา: DEV Community

4 hours ago 11 นาที
3 views
วิธีดึงข้อมูลราคาจาก Google Hotels ด้วย API สำหรับนักพัฒนา

วิธีดึงข้อมูลราคาจาก Google Hotels ด้วย API สำหรับนักพัฒนา

อยากทำแอปท่องเที่ยวแต่ดึงข้อมูลราคาจาก Google Hotels ไม่ได้? มาดูวิธีใช้ Apify Actor ช่วยดึงข้อมูลแบบอัตโนมัติด้วย Python ง่ายๆ ไม่ต้องกลัวเว็บพัง

ที่มา: DEV Community

7 hours ago 8 นาที
5 views
วิธีเช็กความพร้อมโปรเจกต์ก่อนปล่อยงานจริงด้วย ReleaseReady

วิธีเช็กความพร้อมโปรเจกต์ก่อนปล่อยงานจริงด้วย ReleaseReady

เคยไหม? โค้ดรันได้ในเครื่องแต่พอปล่อยจริงกลับพัง! มาดูวิธีตรวจสอบความพร้อมของโปรเจกต์ก่อนอัปขึ้น GitHub ด้วยเครื่องมือ ReleaseReady กัน

ที่มา: DEV Community

11 hours ago 9 นาที
5 views