Home | О проекте | Новости | Рекламные статейки

Безопасное Совместное Использование Объектов с Блокировкой

Рабочая среда для версии 3.x или 4.x Netscape-сервера является многопоточной; то есть она обрабатывает более одного запроса в единицу времени. Поскольку эти запросы могут требовать выполнения JavaScript, то более чем один поток выполнения JavaScript может быть активным в одно и то же время.

1.Рекламка: s |

Если несколько потоков одновременно пытаются изменить свойство одного и того же объекта JavaScript, они могут привести этот объект в несоответствующее состояние. Участок кода, в котором необходимо выполнять один, и только один, поток выполнения в единицу времени, называется критическим разделом/сritical section.

2.Рекламка: s | ;
x |   

Error. Page cannot be displayed. Please contact your service provider for more details. (31)


Один объект server используется совместно всеми клиентами и всеми приложениями, работающими на сервере. Один объект project используется всеми клиентами, получающими доступ к одному приложению на сервере. Кроме того, Ваше приложение может создавать другие объекты, которые оно предоставляет в совместное пользование клиентским запросам, или оно даже может совместно с другими приложениями использовать объекты. Для поддержания целостности данных в этих совместно используемых объектах Вы обязаны получить исключительный доступ к объекту, прежде чем изменять любое его свойство.

В отличие от предыдущих релизов, неявная блокировка объектов project и server теперь отсутствует.

Чтобы лучше понять, что происходит, рассмотрим следующий пример. Предположим, Вы создаёте совместно используемый объект project.orders для отслеживания заказов пользователей. Вы обновляете project.orders.count каждый раз при получении нового заказа, используя следующий код:

var x = project.orders.count;
x = x + 1;
project.orders.count = x;

Предположим, что project.orders.count первоначально установлено в 1 и что поступили два новых заказа в двух разных потоках. Произойдёт следующее:

  1. Первый поток сохраняет project.orders.count в переменной x.
  2. Прежде чем продолжить, второй поток запускается и сохраняет то же самое значение в своей копии переменной x.
  3. С этого момента оба потока имеют значение 1 в x.
  4. Второй поток завершает своё выполнение и устанавливает project.orders.count в 2.
  5. Первый поток продолжает выполнение, не зная, что значение project.orders.count изменилось, и также устанавливает 2 в х.

Итак, конечное значение project.orders.count будет 2, хотя корректным должно быть 3.

Чтобы избежать проблем такого рода, Вам нужно получить исключительный доступ к свойствам совместно используемых объектов перед тем как записывать в них. Для этих целей Вы можете конструировать Ваши собственные экземпляры класса Lock, работающие с любым совместно используемым объектом. Кроме того, объекты server и project имеют методы lock и unlock, которые Вы можете использовать для ограничения доступа к этим объектам.

 
Использование Экземпляров Класса Lock

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

В терминах программирования замок/lock представлен экземпляром класса Lock. Вы можете использовать экземпляр класса Lock для получения исключительного доступа к любому совместно используемому объекту. Обычно Вы создаёте экземпляры Lock на начальной странице Вашего приложения (по причинам, которые будет изложены позднее).

На других страницах, перед критичным для совместно используемого объекта разделом (например, перед разделом, который запрашивает и изменяет значение свойства), Вы вызываете метод lock экземпляра Lock. Если этот метод возвращает true, Вы получаете замок и можете продолжать. В конце критичного раздела Вы вызываете метод unlock Lock -экземпляра.

Когда клиентский запрос в одиночном потоке выполнения вызывает метод lock, любой другой запрос, вызывающий метод lock для того же Lock -экземпляра, ожидает, пока первый поток не вызовет метод unlock, пока не закончится таймаут или пока не возникнет ошибка. Это верно независимо от того, находится второй запрос в другом потоке для того же клиента или в потоке для другого клиента.

Если все потоки вызывают метод lock перед попыткой изменения совместно используемого объекта, то лишь один поток в единицу времени может войти в критичный раздел.

Использование замков находится всецело под управлением разработчика и требует кооперации. Машина выполнения не заставляет Вас ни вызывать lock, ни учитывать блокировку, полученную кем-либо другим. Если Вы не спрашиваете, Вы можете изменять всё что захотите. Поэтому очень важно выработать привычку всегда вызывать lock и unlock при входе и выходе из критичного раздела кода и проверять return-значение метода lock, чтобы гарантировать, что блокировка получена. Можно представлять это в терминах флага: если Вы не запрашиваете флаг, вы не будете находиться в режиме ожидания. Если Вы не находитесь в режиме ожидания, Вы можете изменять то, что изменять нельзя.

Вы можете создать столько замков, сколько Вам необходимо. Один и тот же замок может использоваться для управления доступом к нескольким объектам, либо каждый объект (или даже каждое свойство) может иметь собственный замок.

Замок/lock сам по себе является просто объектом JavaScript; Вы можете сохранить ссылку на него в любом другом объекте JavaScript. Таким образом, например, обычной практикой является конструирование экземпляра Lock и сохранение его в объекте project.

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

Следующий код показывает, как отследить заказы потребителей в совместно используемом объекте project.orders, рассмотренном ранее, и как обновлять project.orders.count каждый раз при получении нового заказа. Включите в начальную страницу приложения такой код:

// Создать новый Lock и сохранить в project.
project.ordersLock = new Lock();
if (! project.ordersLock.isValid()) {
// Невозможно создать Lock. Перенаправить на страницу обработки ошибок.
redirect ("sysfailure.htm");
}

Этот код создаёт экземпляр класса Lock и проверяет (вызовом метода isValid), не возвращено ли что-нибудь неправильное при его создании. Очень редко Ваш экземпляр Lock конструируется неправильно. Это случается только тогда, когда машина выполнения запущена вне системных ресурсов при создании объекта.

Вы обычно создаёте экземпляры Lock на начальной странице, поэтому Вам не нужно получать замок перед созданием экземпляров Lock. Начальная страница запускается только один раз - при старте приложения на сервере. Поэтому Вам гарантируется, что создаётся только один экземпляр каждого замка.

Если, однако, Ваше приложение создаёт замок на какой-либо иной странице, множественные запросы могут вызывать эту страницу в это время. Один запрос может проверять наличие замка и не обнаружить его, в то время как другой запрос создаёт замок, а третий запрос создаёт второй замок. Тем временем первый запрос вызывает метод lock своего объекта. Затем второй запрос вызывает метод lock своего объекта. Оба запроса теперь "думают", что они имеют безопасный доступ к критичному разделу кода и продолжают свою работу, нарушая работу другого.

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

// Начало критичного раздела -- получить замок.
if ( project.ordersLock.lock() )
{
var x = project.orders.count;
x = x + 1;
project.orders.count = x;
// Конец критичного раздела -- освободить замок.
project.ordersLock.unlock();
}
else
redirect("combacklater.htm");

Этот код запрашивает замок. Если замок получен (то есть, если метод lock возвратил true), выполняется вход в критичный раздел, вносятся изменения и, наконец, замок освобождается. Если метод lock возвращает false, то данный код не получает замка. В этом случае приложение перенаправляет на страницу, которая сообщает, что приложение в данный момент не может выполнить запрос.

 
Специальные Замки для Объектов project и server

Каждый из объектов project и server имеет методы lock и unlock. Вы можете использовать эти методы для получения исключительного доступа к свойствам этих объектов.

В этих методах ничего нового нет. Вам также необходима кооперация с другими участками кода. Вы можете представлять эти методы как имеющие флаги: один флаг с именем "project", а другой - флаг с именем "server." Если другой раздел кода не вызывает project.lock, первый может изменять любые свойства объекта project.

В отличие от метода lock класса Lock, Вы не можете специфицировать таймаут для метода lock объектов project и server. То есть, когда Вы вызываете project.lock, система ожидает бесконечно долго освобождения замка. Если Вы хотите ожидать только в течение определённого периода, используйте экземпляр класса Lock.

В примере использованы методы lock и unlock для получения исключительного доступа к объекту project для модификации свойства ID потребителя:

project.lock()
project.next_id = 1 + project.next_id;
client.id = project.next_id;
project.unlock();

 
Исключение Мёртвой Блокировки/Deadlock

Вы используете замки для защиты критичных участков кода. На практике это означает, что один запрос ожидает, пока другой выполняет критичный код. Вы обязаны соблюдать осторожность при использовании замков для защиты критичных разделов. Если один запрос ожидает освобождения замка, полученного другим запросом, а этот второй запрос ожидает освобождения замка, полученного первым запросом, ни один из запросов не сможет продолжить работу. Эта ситуация называется deadlock/тупик/мертвая блокировка.

Рассмотрим предыдущий пример обработки заказов потребителей. Предположим, что приложение разрешает два действия. В одном - пользователь вводит нового потребителя; в другом - пользователь вводит новый заказ. Как часть создания нового потребителя приложение также создаёт новый заказ потребителя. Это действие выполняется на одной странице приложения, давая примерно такой код:

// Создать нового потребителя (customer).
if ( project.customersLock.lock() ) {
var id = project.customers.ID;
id = id + 1;
project.customers.ID = id;
// Стартовать новый заказ (order) для этого нового потребителя.
if ( project.ordersLock.lock() ) {
var c = project.orders.count;
c = c + 1;
project.orders.count = c;
project.ordersLock.unlock();
}
project.customersLock.unlock();
}

Во втором типе действия пользователь вводит новый заказ потребителя. Как часть процесса ввода нового заказа: если потребитель ещё не является зарегистрированным потребителем, приложение создаёт нового потребителя. Это действие выполняется на другой странице приложения, где может быть примерно такой код:

// Стартовать новый заказ.
if ( project.ordersLock.lock() ) {
var c = project.orders.count;
c = c + 1;
project.orders.count = c;
if (...код определения неизвестного потребителя...) {
// Создать нового потребителя.
// Этот внутренний замок может вызвать проблемы!
if ( project.customersLock.lock() ) {
var id = project.customers.ID;
id = id + 1;
project.customers.ID = id;
project.customersLock.unlock();
}
}
project.ordersLock.unlock();
}

Заметьте, что каждый из этих фрагментов кода пытается получить второй замок, уже получив один. Это может вызвать проблемы. Предположим, что один поток начинает создание нового потребителя; он получает замок customersLock. В это же самое время другой поток начинает создание нового заказа; он получает замок ordersLock. Теперь первый поток запрашивает замок ordersLock. Поскольку второй поток уже получил этот замок, первый поток должен ждать. Предположим, однако, что второй поток теперь запрашивает замок customersLock. Первый поток уже имеет этот замок, поэтому второй поток должен ждать. Теперь потоки ждут друг друга. Поскольку никто их них не специфицировал таймаут, оба они будут ждать бесконечно.

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

// Создать нового потребителя.
if ( project.customersLock.lock() ) {
var id = project.customers.ID;
id = id + 1;
project.customers.ID = id;
project.customersLock.unlock();
}
// Стартовать новый заказ для этого нового потребителя.
if ( project.ordersLock.lock() ) {
var c = project.orders.count;
c = c + 1;
project.orders.count = c;
project.ordersLock.unlock();
}

Второй фрагмент будет примерно таким:

// Стартовать новый заказ.
if ( project.ordersLock.lock() ) {
var c = project.orders.count;
c = c + 1;
project.orders.count = c;
project.ordersLock.unlock();
}
if (...код для определения неизвестного потребителя...) {
// Создать нового потребителя.
if ( project.customersLock.lock() ) {
var id = project.customers.ID;
id = id + 1;
project.customers.ID = id;
project.customersLock.unlock();
}
}

Хотя это и надуманная ситуация, тупики это совершенно реальная проблема, и они могут произойти во многих случаях. Для этого даже не понадобится более одного замка или более одного запроса. Рассмотрим код, в котором две функции запрашивают один и тот же замок:

function fn1 () {
if ( project.lock() ) {
// ... какие-то действия ...
project.unlock();
}
}
function fn2 () {
if ( project.lock() ) {
// ... какие-то другие действия ...
project.unlock();
}
}

Сам по себе этот код не содержит проблем. Позднее слегка измените его, чтобы fn1 вызывала fn2, уже имея замок, как показано далее:

function fn1 () {
if ( project.lock() ) {
// ... какие-то действия ...
fn2();
project.unlock();
}
}

Вот вы и получили тупик/deadlock. Это, конечно, немного смешно, когда единственный запрос ожидает от самого себя освобождения флага!


footer:
Rambler's Top100