Для того чтобы исключить подобный сценарий,
автор многопотокового приложения должен решать
проблему синхронизации при попытке
одновременного доступа к разделяемым ресурсам.
Если говорить о файлах с совместным доступом, то
сходная ситуация может возникнуть и при
столкновении различных процессов, а не только
потоков одного процесса. Разработчика в этом
случае уже не устроит стандартный способ
открытия файла. Например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). |