Стратегии
решения проблемы
Для того чтобы
исключить подобный сценарий, автор многопотокового приложения должен решать
проблему синхронизации при попытке одновременного доступа к разделяемым ресурсам.
Если говорить о файлах с совместным доступом, то сходная ситуация может возникнуть
и при столкновении различных процессов, а не только потоков одного процесса.
Разработчика в этом случае уже не устроит стандартный способ открытия файла.
Например
1:
//=======
Создаем объект класса CFile
CFile
file;
//
====== Строка с именем файла
CString
fn("MyFile.dat");
//=====
Попытка открыть файл для чтения
if
( ! file.Open(fn,CFile::modeRead))
{
MessageBox
("He могу открыть файл "+fn, "Ошибка");
return;
}
Он должен писать
код с учетом того, что файл может быть заблокирован какое-то время другим процессом.
Если следовать уже рассмотренной тактике ожидания ресурса в течение какого-то
времени, то надо создать код вида:
bool
CMyWnd::TryOpen()
<
//======
Попытка открыть файл и внести изменения
CFile
file;
CString
fn("MyFile.dat"), Buffer;
//=====
Флаг первой попытки
static
bool bFirst =
true;
if
(file.Open (fn, CFile:: modeReadWrite I CFile::shareExclusive))
{
//
Никакая другая программа не сможет открыть
//
этот файл, пока мы с ним работаем
int
nBytes = flie.Read(Buffer,MAX_BYTES);
//====
Работаем с данными из строки Buffer
//====
Изменяем их нужным нам образом
//====
Пришло время вновь сохранить данные
file.Write(Buffer,
nBytes);
file.
Close ();
//====
Начиная с этого момента, файл доступен
//====
для других процессов
//====
Если файл был открыт не с первой попытки,
//====
то выключаем таймер ожидания
if
(IbFirst)
KillTimer(WAIT_ID);
//=====
Возвращаем флаг успеха
return
bFirst =
true;
}
//======
Если не удалось открыть файл
else
if
(bFirst) // и эта неудача — первая,
//=====
то запускаем таймер ожидания
SetTiraer(WAIT_ID,
1000, 0);
//=====
Возвращаем флаг неудачи
return
bFirst =
false;
}
В другой функции,
реагирующей на сообщения таймера, называемой, как вы знаете, функцией-обработчиком
(Message Handler), надо предусмотреть ветвь для реализации выбранной
тактики ожидания:
//======
Обработка сообщений таймера
void
CMyWnd::OnTimer(UINT nID)
{
//======
Счетчик попыток
static
int iTrial = 0;
//======
Переход по идентификатору таймера
switch
(nID)
{
//==
Здесь могут быть ветви обработки других таймеров
case
WAIT_ID:
//======
Если не удалось открыть
if
(ITryOpenO)
{
//=====
и запас терпения не иссяк,
if
(++iTrial < 10)
return;
// то продолжаем ждать
//===
Если иссяк, то сообщаем о полной неудаче
else
{
MessageBox
("Файл занят более 10 секунд",
"Ошибка");
//====== Отказываемся ждать
KillTimer(WAIT_ID);
//======
Обновляем запас терпения
iTrial
= 0;
}
}
}
}
Существуют
многочисленные варианты рассмотренной проблемы, и в любом случае программист
должен решать их, например путем синхронизации доступа к разделяемым ресурсам.
Большинство коммерческих систем управления базами данных умеют заботиться о
целостности своих данных, но и вы должны обеспечить целостность данных своего
приложения. Здесь существуют две крайности: отсутствие защиты или ее слабость
и избыток защиты. Вторая крайность может создать низкую эффективность приложения,
замедлив его работу так, что им невозможно будет пользоваться. Например, если
в примере с повышением зарплаты первый поток заблокирует-таки доступ к записи,
но затем начинает вычислять новое значение зарплаты, обратившись к источнику
данных о средней (в отрасли) зарплате по всей стране. Такое решение проблемы
может привести к ситуации, когда второй поток процесса, который готов корректировать
эту же запись, будет вынужден ждать десятки минут.
Одним из более
эффективных решений может быть такое: первый поток читает запись, вычисляет
прибавку и только после этого блокирует, изменяет и освобождает запись. Такое
решение может снизить время блокирования до нескольких миллисекунд. Однако защита
данных теперь не сработает в случае, если другой поток поступает также. Второй
поток может прочитать запись после того, как ее прочел первый, но до того, как
первый начал изменять запись. Как поступить в этом случае? Можно, например,
ввести механизм слежения за доступом к записи, и если к записи было обращение
в интервале между чтением и модификацией, то отказаться от модификации и повторить
всю процедуру вновь.
Примечание
Каждое решение создает
новые проблемы, а поиск оптимального баланса ложится на плечи программиста,
делая его труд еще более интересным. Кстати, последнее решение может вызвать
ситуацию, сходную с той, когда два человека уступают друг другу дорогу. Отметьте,
что решение вопроса кроется в балансе между производительностью (performance)
и целостностью данных (data integrity).