Почему одновременная копия файла не дает сбоя?

У меня есть простой сценарий, который я предполагал, что он потерпит неудачу. 5 процессов пытаются скопировать (используя shutil.copy) один и тот же большой файл в одно и то же место (с тем же именем). Я полагаю, что это не удастся, потому что один из процессов откроет файл для записи в него, а остальные процессы завершатся с ошибкой, так как файл уже был открыт. Но этого не произошло, и все процессы заканчиваются правильно. Я также сделал md5 из скопированного файла, и, кажется, все в порядке.

Я хочу знать, является ли это стабильным поведением или я делаю что-то не так или странно, чтобы избежать возможных ошибок в будущем.

Я также попробовал тот же скрипт с другими функциями копирования. С win32file.CopyFile сценарий завершается с ошибкой, как и ожидалось (четыре процесса получают исключение: процесс не может получить доступ к файлу, потому что он используется другим процессом.).

Все тесты были сделаны на окнах.

Здесь вы можете найти скрипт, который я использую:

import os
import shutil
import hashlib
import win32file
import multiprocessing
from datetime import datetime

import logging
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)

ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

log.addHandler(ch)


NUM_PROCESS = 5
FILE = r"C:\Test\test.txt"


def worker(full_path):
    try:
        worker_name = multiprocessing.current_process().name

        std_path = full_path
        std_dst = r"C:\Test\download\test.txt"

        log.debug("[%s][%s] Copying to: %s to %s", str(datetime.now()), worker_name, std_path, std_dst)
        shutil.copy(std_path, std_dst)
        # win32file.CopyFile(std_path, std_dst, False)
        log.debug("[%s][%s] End copy", str(datetime.now()), worker_name)

        log.info("[%s][%s] Copy hash: %s" % (str(datetime.now()), worker_name, hashfile(open(std_dst, 'rb'),
                                                                                        hashlib.md5())))
        return
    except Exception as e:
        log.error("Can't copy file %s to %s. %s" % (std_path, std_dst, e))


def createEmptyFile(full_path, size):
    try:
        with open(full_path, "wb") as f:
            step = size / 1024 + 1
            if step < 2:
                step = 2
            for _ in xrange(1, step):
                f.write("\0" * 1024)

    except Exception as e:
        raise Exception("Error creating empty file: %s" % e)


def hashfile(afile, hasher, blocksize=65536):
    buf = afile.read(blocksize)
    while len(buf) > 0:
        hasher.update(buf)
        buf = afile.read(blocksize)
    return hasher.hexdigest()


if __name__ == '__main__':
    path = FILE
    os.chdir(os.path.dirname(path))

    if not os.path.exists(FILE):
        log.info("Creating test file")
        createEmptyFile(path, 9589934595)

    log.debug("Creating file hash")
    log.info("Origin hash: %s" % hashfile(open(path, 'rb'), hashlib.md5()))

    pool = multiprocessing.Pool(NUM_PROCESS)
    result = pool.map(worker, [path] * NUM_PROCESS)

1 ответ

Решение

shutil.copy функция использует shutil.copyfile функция, которая использует простой open() ( см. источники)

Согласно документации для open() функция, он будет обрезать файл, если он уже существует. Так open() позволяет одновременно записывать в файл и не блокирует его, как вы могли ожидать.

Когда последний работник выполняет open() функция, он усекает файл, в то время как другие работники продолжают писать вперед. Последний работник добавляет данные, которые он может усечь, а затем перезаписывает все записанные другими работниками. Когда первый работник заканчивает копирование файла, уже скопированы все данные, поэтому вычисленный хэш копируемого файла должен быть таким же, как и исходный. Оставшиеся работники просто перезаписывают существующие данные, но файл не изменит ни его хэш.

Если вы хотите заблокировать файл для копирования, вы должны использовать flock() самостоятельно.

Другие вопросы по тегам