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