Поиск

Блокировка данных

Представьте себе, что вы написали на Perl замечательную программу, которой будут пользоваться многие и многие люди. Независимо от того, в какой операционной системе вы планируете ее эксплуатировать (UNIX, Windows NT или даже Windows 9x), возможны ситуации, когда несколько человек попытаются одновременно запустить вашу программу. А если вы предполагаете поместить программу на Web-сервер, она может запускаться так часто, что в памяти вообще одновременно будет находиться несколько копий программы.

А теперь предположим, что вашей программе для работы необходимы данные, хранящиеся, например, в текстовом файле, который был описан выше. Тип файла данных здесь не играет особой роли, поскольку все сказанное ниже можно применить к любому типу баз данных. Рассмотрим приведенный ниже фрагмент кода, в котором используются функции, описанные в предыдущем разделе.

Вроде бы все выглядит внешне безобидно, не правда ли? Все так, только проблемы начинаются, когда два или несколько человек одновременно запустят вашу программу на выполнение и попытаются добавить в файл новые записи. При этом программа напрочь перестает работать. Ниже приведена диаграмма выполнения программы двумя пользователями, причем второй пользователь начал свою работу сразу после первого. Проанализируйте ее внимательно.

С точки зрения первого пользователя, данные читаются на втором шаге, новая запись "Дмитрий 555-1212" добавляется в массив @PHONEL на третьем шаге, а на четвертом шаге содержимое массива @PHONEL записывается в файл базы данных.

С точки зрения второго пользователя, данные читаются на третьем шаге, новая запись "Юрий 555-6611" добавляется в массив @PHONEL на четвертом шаге, а на пятом шаге содержимое массива @PHONEL записывается в файл базы данных.

Ошибка здесь вот в чем: данные, которые читает второй пользователь на третьем шаге, не содержат записи "Дмитрий 555-1212", поскольку первый пользователь еще не успел ее добавить в файл. Таким образом, второй пользователь добавляет запись "Юрий 555-6611" в массив. @PH0NEL, а в это время первый пользователь записывает в файл базы данных содержимое массива @PHONEL, в котором уже есть запись "Дмитрий 555-1212".

Когда же копия программы второго пользователя "добирается" до пятого шага, она "затирает" данные, записанные первым пользователем. Таким образом, в окончательном варианте базы данных на диске будет присутствовать запись "Юрий 555-6611", но не будет записи "Дмитрий 555-1212", что является очевидной ошибкой.

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

Описанная выше работа программы в многозадачной среде чем-то напоминает гонки "Формулы 1". Отлаживать многозадачные и многопоточные приложения очень сложно, поскольку работа таких программ зависит как от количества запушенных копий, так и от многих других факторов. Поэтому ошибки в многопоточных программах не всегда очевидны и их очень трудно выявить и локализовать.

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

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

Блокировка в UNIX и Windows NT

Для блокировки файлов в системах UNIX и Windows NT используется функция Perl flock, в которой реализован так называемый совещательный механизм блокировки. Это означает, что любая программа, которая хочет записать что-либо в файл, должна перед этим вызвать функцию flock и убедиться, что никакая другая программа в данный момент времени ничего не пишет в этот же файл. Естественно, при желании любая программа сможет в любой момент записать данные в файл, не прибегая к средствам блокировки. В этом заключается отличие совещательного механизма блокировки от принудительного.

С механизмом совещательной блокировки вы должны быть уже хорошо знакомы. Вспомните, как работает светофор на перекрестке. Красный сигнал светофора запрещает движение транспорта и таким образом препятствует движению машин в пересекающихся направлениях. Но регулировка с помощью светофора работает только тогда, когда все водители строго соблюдают правила дорожного движения. То же самое можно сказать и о механизме блокировки файлов. Любая программа, в которой не исключена возможность доступа к файлам одновременно с другими программами, должна использовать функцию flock для предотвращения нежелательных последствий. Следовательно, механизм совещательной блокировки не препятствует доступу нескольких программ к файлу, а предотвращает только возможность получения права доступа к файлу.

У функции flock предусмотрены два параметра — дескриптор файла и тип блокировки, как показано ниже.

Функция flock возвращает истинное значение, если блокировка файла была успешно выполнена. В противном случае возвращается ложное значение. Иногда вызов функции flock приостанавливает выполнение программы до момента снятия других блокировок. Ниже мы остановимся на этом более подробно. Директива use Fcntl qw(; flock) позволяет использовать символические имена вместо трудно запоминаемых цифр при определении типа блокировки.

Существуют два вида блокировки: совместно используемая и монопольная. Обычно при чтении файла применяется совместно используемый тип блокировки, а при записи — монопольный. Если какой-либо процесс получил монопольный доступ к файлу, он может выполнять с ним любые действия, поскольку в этот момент никакой другой процесс не сможет обратиться к файлу. Однако многие процессы могут совместно использовать один и тот же файл для чтения, если никакой другой процесс не запросил монопольный доступ к этому файлу.

Ниже мы приведем несколько значений параметра типа блокировки для функции flock.

  • LOCK_SH — это значение определяет совместно используемый тип блокировки файла. Если какой-либо другой процесс запросил монопольную блокировку, вызов функции flock с параметром LOCK_SH приведет к приостановке выполнения программы до момента снятия монопольной блокировки. И только после того, как монопольная блокировка будет снята, выполняется совместно используемый тип блокировки.
  • LOCK_EX — это значение определяет монопольный тип блокировки файла, открытого для записи. Если какие-либо процессы ранее заблокировали этот файл (для монопольного или совместно используемого доступа), вызов функции flock с параметром LOCK EX приведет к приостановке выполнения программы до момента снятия всех блокировок.
  • LOCK UN — это значение позволяет отменить блокировку файла. Следует отметить, что используется оно сравнительно редко, поскольку при закрытии файла автоматически все не сохраненные данные записываются на диск и отменяется действие всех блокировок. Учтите, что принудительное снятие блокировки с открытого файла может привести к потере данных или нарушению структуры файла.
Блокировки файла, выполненные с помощью функции flock, автоматически отменяются при закрытии файла либо при завершении работы программы (даже если программа завершилась с ошибкой).

При выполнении блокировки файла, который открыт для чтения и записи, могут возникнуть некоторые нюансы. Проблема заключается в том, что открытие файла и выполнение последующей блокировки — двухступенчатый процесс. Другими словами, для того чтобы заблокировать файл, сначала его нужно открыть. Поэтому, если вы откроете файл с помощью оператора open(FH, ">filename"), а затем сразу же попытаетесь его заблокировать с помошью функции flock, содержимое файла будет затерто перед выполнением блокировки (поскольку при открытии использовался оператор усечения файла >). Таким образом, при открытии файла для записи вы можете случайно модифицировать файл, который был заблокирован другими процессами.

Для решения описанной выше проблемы используется так называемый семафорный файл (обычный файл, содержимое которого не имеет особого значения; он используется только для выполнения блокировок).

Перед использованием семафорного файла вы должны определиться с его именем. Для выполнения и снятия блокировок используются уже знакомые нам функции, как показано в листинге 15.3. Этот фрагмент кода нельзя назвать законченной программой, но его вполне можно включить в другой проект как самостоятельную часть.

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

Функции get_lock() и release_lock(), рассмотренные выше, мы будем при необходимости использовать далее по всей книге для выполнения блокировки файлов.

Не рекомендуется устанавливать блокировку перед вводом информации с клавиатуры или для выполнения любой другой медленной операции, поскольку при этом будут приостановлены все другие процессы, ожидающие снятия блокировки. Старайтесь блокировать критичные участки кода только на короткий промежуток времени.
Чтение и запись файлов с блокировкой

А теперь настало время продемонстрировать работу функций readdata() и writedataO, выполняющих блокировку текстовой базы данных. Для этого нам нужно определить имя файла-семафора и добавить в код вызовы функций get_lock() и release lock{), описанных в предыдущем разделе. Код этих функций мы поместили в начало листинга 15.4.

Большая часть кода из листинга 15.4 вам уже должна быть хорошо знакома функции get_lock{), release_lock(), readdata() и writedata() были описаны выше на этом занятии.

Основная часть программы начинается со строки 34. Сначала выполняется блокировка файла с помощью функции get_lock(). Затем с помощью функции readdata() содержимое файла базы данных помещается в массив @PHONEL, выполняется добавление новой записи и содержимое массива записывается обратно в файл базы данных с помощью функции writedata(). После того как будут выполнены все операции, блокировка снимается с помощью функции release_lock(). В результате другие программы смогут получить доступ к нашей базе данных.

Блокировка в Windows9x

Как оказалось, в системах Windows 95/98 не поддерживается блокировка файлов. Почему это происходит? Причина заключается в том, что в этих операционных системах только одной программе разрешается открывать файл по записи в данный момент времени, поэтому блокировка оказывается ненужной. В результате, если выполнить функцию flock в системе Windows 9x, будет выдано следующее сообщение об ошибке:

К сказанному выше остается добавить, что операционная система Windows 9x является однопользовательской.

В этой книге в листингах примеров, связанных с блокировкой, используются функции get_lock() и release_lock(). Использование этих функций в системах Windows 95 и 98 вызывает появление сообщения об ошибке, поскольку, как уже было сказано выше, в этих операционных системах функция flock не реализована. Поэтому, чтобы примеры работали без ошибок, просто закомментируйте вызовы функций get_lock() и release_lock{). Для напоминания в текст листингов будут включены соответствующие комментарии.
Блокировка в системах UNIX и Windows NT

В некоторых случаях требуется, чтобы несколько программ одновременно могли читать и записывать данные в файл, однако при этом функция flock по каким-либо причинам оказывается недоступной. И даже на тех платформах, где функция flock реализована, бывают ситуации, когда ею нельзя воспользоваться. Например, в системах UNIX эта функция не работает в сетевой файловой системе (NFS). Кроме того, часто программы запускаются в смешанной среде с выделенным сервером на базе UNIX и рабочими станциями на базе Windows. При этом функция flock поддерживается в системах UNIX и не поддерживается в системах Windows.

За более подробной информацией о блокировке файлов и функции flock обратитесь к разделу 5, "Files and Formats", списка часто задаваемых вопросов по Perl. Для этого откройте раздел perlfaq5 электронной документации по Perl.