Одноэкземплярные приложения на Qt

в 13:42, , рубрики: qt, Qt Software, метки:

Сейчас я активно пишу под Qt. И встала у меня задачка проверить при запуске приложения, а не запущено ли оно уже. Под линуксом. Велосипеды городить не хотелось, хотелось родить что-то используя готовые классы Qt, чтобы сразу было кроссплатформенно. В винде, например, есть готовое решение — именованные мьютексы (или семафоры, неважно, главное именованные). Стал гуглить. Ага, в Qt есть нечто подобное, называется QSystemSemaphore. Работает так:

— создаем экземпляр класса QSystemSemaphore, он создает глобальный (системный) семафор
— вызываем acquire, уменьшаем счетчик семафора на единицу
— что-то делаем
— вызовом release, увеличиваем счетчик семафора на единицу

Вобщем класический семафор, только глобальный. Понятно, что если создать семафор с изначальным значением счетчика равным 1 и вызвать acquire, то пока мы не вызовем release никто больше не сможет этот семафор захватить. Ура, решение найдено! Вырисовывается сразу следующий код одноэкземплярного приложения:

— создаем семафор
— захватываем
— приложение работает
— после завершение работы приложение освобождает семафор

Все здорово, все прекрасно. Документация обещает нам, что даже если не вызвать release (например программа скрашилась) система все равно вернет в семафор все захваченные у него единицы. Но возникает проблема. Если пользователь запустит второй экземплар приложения — как он об этом узнает? Вызов метода acquire блокирующий и пока семафор не освободиться приложение будет «висеть», а пользователь в недоумении чего-то ждать.

Казалось бы — не проблема. Например тот же QMutex помимо вызова lock для захвата мьютекса имеет вызов и try_lock для неблокирующей попытки захвата. Должно что-то подобное быть и у системного семафора. Но — увы — его нет. Я долго и безуспешно гуглил — на форумах полно подобных тем и ни одного работающего решения. Проблема? Проблема. Интересная? Весьма. Я взялся ее решать.

При помощи одного QSystemSemaphore очевидно ничего не разрешится. Что в Qt еще есть из глобального? Еще есть QSharedMemory. Разделяемая между разными приложениями память. Работает так:

— создаем экземпляр класса QSharedMemory
— вызываем attach для подсоединения к уже созданной памяти
— если подсоединитсья не удалось — вызываем create чтобы создать разделяемую память
— работаем с разделяемой памятью (например передаем какие-то данные в другой процесс)
— деструктор сам вызывает detach и отсоединение от разделяемой памяти

Казалось бы — вот оно! Если вызов attach прошел успешно, значит приложение уже запущено. Если нет — приложение запущено впервые, вызываем create и, таким образом, закрепляем свое право первенства в вопросе владения разделяемой памятью (т.е. у всех остальных attach будет происходить успешно уже). Проблемы здесь, собственно, две.

Первая — гонки. Два процесса могут одномервменно вызвать attach, потрепеть неудачу, оба будут считать что они — первые и вызовут create. Один создаст память, другой — потерпит неудачу. Нехорошо, но, вобщем, терпимо. Можно обойти.

Вторая проблема посерьезнее. Оказывается линуксовая реализация QSharedMemory устроена так, что если для памяти не будет вызван detach она не разрушается. Проще говоря, если процесс присоединился к разделяемой памяти, а затем аварийно завершился, то память останется созданной. И следующий вызов attach пройдет успешно. Т.е. аварийное завершение единственного экземпляра приложения приведет к тому, что больше ни одного экземпляра приложения создано не будет. Мда.

Таким образом имеем два класса — QSystemSemaphore и QSharedMemory, каждый из которых отчасти реализует нужную функциональность, но полностью конечную задачу не решает. Может быть удасться два этих «недокласса» гармонично сочетать?

И таки да! QSystemSemaphore решает первую проблему QSharedMemory, проблему гонок. Код:

QSustemSemaphore sema("<Unique name1>", 1);
bool isRunning;
sema.acquire();

QSharedMemory shmem("<Unique name2>");
if (shmem.attach())
{
    isRunning = true;
}
else
{
    shmem.create(1);
    isRunning = false;
}

sema.release();

Проще говоря глобальный семафор синхронизирует управление разделяемой памятью. Как говорится, элементарно, Ватсон.

Вторая проблема, как оказалось, тоже имеет решение. Довольно смешное. Чтобы линуксовая реализация разделяемой памяти проверила счетчик ссылок на нее и разрушила ее, если ссылок больше нет, достаточно присоединиться к ней и отсоединиться. И если в результате аварийного завершения предыдущего экземпляра приложения осталась неразруженая разделяемая память с нулевым (фактически) счетчиком ссылок подсоединение к ней и немедленное отсоединение разрушит эту память. А если счетчик ссылок ненулевой — он останется прежним. Что, вобщем, и требуется.

Окончательно код выглядит так:

QSustemSemaphore sema("<Unique name1>", 1);
bool isRunning;
sema.acquire();

{
    QSharedMemory shmem("<Unique name2>");
    shmem.attach();
}

QSharedMemory shmem("<Unique name2>");
if (shmem.attach())
{
    isRunning = true;
}
else
{
    shmem.create(1);
    isRunning = false;
}

sema.release();

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

Автор: gcooler

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js