On-Line Библиотека www.XServer.ru - учебники, книги, статьи, документация, нормативная литература.
       Главная         В избранное         Контакты        Карта сайта   
    Навигация XServer.ru


Бетонные блоки изготовление - домашнее изготовление бетонных блоков www.vnukovoabz.ru.




 

Ограничение количества одновременно запущенных экземпляров приложения

Владимир Юдин

Обсуждение данной темы ведется, начиная с появления первых 32-х разрядных версий Windows. Казалось бы, проблема давно уже должна быть окончательно решена, но количество вопросов в конференциях и форумах не уменьшается, хотя из книги в книгу, из FAQ'а в FAQ кочуют одни и те же варианты решения. Но не все так очевидно и просто, что подтверждает и вынесенное в эпиграф мнение известного авторитета и эксперта.

Определим требования, которым должно удовлетворять решение для того, что бы его можно было использовать в большинстве случаев. Очевидно, что способ должен:

  1. работать во всех версиях ОС;
  2. быть надежным;
  3. достаточно универсальным;
  4. простым в реализации;
  5. не давать побочных эффектов.

Каким образом можно поступить? На ум сразу приходит решение 'в лоб': почему бы не просмотреть список всех запущенных в системе процессов и не определить, запущен ли уже наш исполняемый модуль? Но почему-то именно этот 'очевидный' способ практически никогда не используется. Для получения списка процессов в Windows 9X используются функции ToolHelp, а в Windows NT - PSAPI. То есть для разных версий Windows и алгоритмы разные. Оставим этот способ для отладочных средств и просмотрщиков:

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

Раз 'в лоб' нельзя, то остается стандартный способ - при запуске приложения проверить какой-то уникальный признак и, если он не установлен, то установить, а после завершения работы сбросить. Именно так на практике обычно и поступают. Кандидатов на роль уникального признака довольно много, но не все одинаково пригодны для нашей задачи. Например, не лучшим решением будет использовать глобальные атомы. В случае аварийного завершения приложения, когда явно не вызывается GlobalFreeAtom, они имеют неприятную особенность оставаться в системе до ее перезагрузки. По той же причине абсолютно неприемлемы: создание или попытка открытия какого-либо файла, использование реестра и т.п. Лучшими кандидатами остаются объекты ядра - они просты в использовании, быстры, счетчик ссылок на объект, как правило, декрементируется даже при аварийном завершении процесса. Чаще всего это Mutex и FileMapping.

Типичный код выглядит так:
program MyProgram;
uses
  Windows,
  Forms,
  MyUnit in 'MyUnit.pas' {Form1};

{$R *.RES}

var
  Mutex : THandle;
begin
  Mutex := CreateMutex(nil, False, 'MyMutex');
  if Mutex = 0 then
    MessageBox(0,'Невозможно создать мьютекс', 'Ошибка',
      MB_OK or MBICONSTOP)
  else if GetLastError = ERROR_ALREADY_EXISTS then
    MessageBox(0,'Программа уже запущена', 'Ошибка',
      MB_OK or MBICONSTOP)
  else
  begin
    Application.Initialize;  
    Application.CreateForm(TForm1, Form1);
    Application.Run;
    CloseHandle(Mutex);
  end;
end.

В приведенном коде все же есть один дефект. Он никогда не проявится в приложениях типа 'Hello World!', но может привести к очень неприятным последствиям в приложениях серьезных. Я имею в виду инициализацию, которая происходит до процедуры проверки при загрузке модулей. Она так же может занимать достаточно много времени. В этих случаях также возможен запуск нескольких экземпляров приложения, что в некоторых случаях может привести к конфликтам доступа и нехватке ресурсов (из-за чего, собственно говоря, чаще всего проверка и производится). Даже если этого и не произойдет, производится бесполезная работа. Отсюда вывод: процедуру проверки необходимо производить как можно раньше, до начала всех инициализаций, и в случае неудачи немедленно завершать приложение. Для этого помещаем процедуру проверки в отдельный модуль, который указываем в списке используемых проектом модулей ПЕРВЫМ:

program MyProgram;
uses
  OneHinst;  
  Windows,
  Forms,
  MyUnit in 'MyUnit.pas' {Form1};

{$R *.RES}

begin
    Application.Initialize;  
    Application.CreateForm(TForm1, Form1);
    Application.Run;
end.

unit OneHinst;
interface

implementation
uses
  Windows;
var
  Mutex : THandle;
  MutexName : array[0..255] of Char;

function StopLoading : boolean;
var
  L,I : integer;
begin
  // В качестве уникального имени мьютекса используем полный путь
  // к исполняемому файлу приложения
  L := GetModuleFileName(MainInstance,MutexName,SizeOf(MutexName));
  // В имени мьютекса нельзя использовать обратные слэши, поэтому
  // заменяем их на прямые
  for I := 0 to L - 1 do
    if MutexName[I] = '\' then
    begin
      MutexName[I] := '/';
    end;
  Mutex := CreateMutex(nil,false,MutexName);

  Result := (Mutex = 0) or // Если мьютекс не удалось создать
  (GetLastError = ERROR_ALREADY_EXISTS); // Если мьютекс уже существует
end;

procedure ShowErrMsg;
const
  PROGRAM_ALREADY_RUN = 'Невозможно запустить программу';
begin
  MessageBox(0,PROGRAM_ALREADY_RUN,MutexName, MB_ICONSTOP or MB_OK);
end;

initialization
  if StopLoading then
  begin
    ShowErrMsg;
    // Так как никаких инициализаций еще не производилось, то
    // спокойно используем для завершения программы Halt -
    // finalization все равно выполнится
    halt;
  end;
finalization
  if Mutex <> 0 then
    CloseHandle(Mutex);
end.

Напоследок еще пример: как ограничить количество одновременно исполняющихся экземпляров приложения

unit LimHinst;
//****************************************************************************
//
// Author: ©2001 Vladimir G. Yudin aka y-soft
// e-mail: y-soft@mail.ru
//
// Description: Ограничение количества одновременно работающих экземпляров 
// приложения.
//
// Отличие от существующих реализаций:
//
// 1. С целью исключения преждевременных инициализаций проверка
// производится в самом начале загрузки приложения, до загрузки всех модулей
// 2. Исключен возможный конфликт имен, т.к. в качестве уникального имени
// используется полный путь к исполняемому модулю
// 3. Изменением значения HinstLim можно установить любое разрешенное количество
// одновременно запущенных экземпляров приложения
// 4. Изменением WaitPause можно регулировать время ожидания 
// (если установить INFINITE, то получится своеобразный вариант горячего
// резервирования)
//
// Тестировалось в WinME и WinNT 4 SP6A. Ошибки не обнаружены
//
// Usage: модуль необходимо указать ПЕРВЫМ в списке uses файла .DPR проекта
// и установить необходимые значения констант HinstLimit и WaitPause.
//
// Возможные расширения:
//
// 1. Значения HinstLimit и WaitPause хранить в INI-файле или в реестре
// 2. Значение HinstLimit  менять динамически в зависимости от условий
//
// Thanks: Спасибо Юрию Зотову за указание на существование проблемы
//
// Disclaimer: Используйте совершенно свободно на свой страх и риск.
// Автор убедительно просит сообщать ему о найденных ошибках и
// внесенных усовершенствованиях.
// Всякие совпадения идей, наименований функций, процедур, переменных и
// констант считать случайными :)
//
//****************************************************************************
interface

const
   //Установите необходимые значения!!!
  HinstLimit = 1;
  WaitPause = 50;

implementation
uses
  Windows;
var
  Semaphore : THandle;
  SemaphoreName : array[0..255] of Char;
  IncCnt : integer;

function StopLoading : boolean;
var
  L,I : integer;
begin
  // В качестве уникального имени семафора используем полный путь
  // к исполняемому файлу приложения (по определению уникален!!!)
  L := GetModuleFileName(MainInstance,SemaphoreName,SizeOf(SemaphoreName));
  // В имени семафора нельзя использовать обратные слэши, поэтому
  // заменяем их на прямые (или еще на что-нибудь кроме #0)
  for I := 0 to L - 1 do
    if SemaphoreName[I] = '\' then
      SemaphoreName[I] := '/';

  Semaphore := CreateSemaphore(nil,HinstLimit,HinstLimit,SemaphoreName);

  Result := (Semaphore = 0) or // Если семафор не удалось создать
  (WaitForSingleObject(Semaphore,WaitPause) <> WAIT_OBJECT_0); 
				// Если семафор занят
end;

procedure ShowErrMsg;
const
  PROGRAM_ALREADY_RUN = 'Лимит исчерпан';

begin
// Главное окно программы еще не существует, поэтому выводим MessageBox 
// без владельца 
 MessageBox(0, PROGRAM_ALREADY_RUN, SemaphoreName, MB_ICONSTOP or 
MB_OK);
end;

initialization
  IncCnt := 0;
  if StopLoading then
  begin
    ShowErrMsg;
    // Так как никаких инициализаций еще не производилось, то
    // спокойно используем для завершения программы Halt -
    // finalization все равно выполнится
    halt;
  end
  else
    IncCnt := 1;
finalization
  if Semaphore <> 0 then
  begin
    // Обязательно явно освобождаем семафор, т.к.
    // автоматически его счетчик ссылок не переустанавливается
    ReleaseSemaphore(Semaphore, IncCnt, nil);
    // Напоследок во избежание неожиданностей освобождаем дескриптор семафора
    // (так предписывает MSDN)
    CloseHandle(Semaphore);
  end;
end.


Языки программирования: разное