Введение
Мьютексы и семафоры решают задачу синхронизации универсально, но не бесплатно: захват и освобождение блокировки — это системный вызов (в худшем случае) с накладными расходами и потенциальным переключением контекста. Для простых операций — таких как инкремент счётчика или установка флага — существует более лёгкая альтернатива: атомарные операции на основе аппаратных инструкций процессора (CAS — compare-and-swap), доступные в Qt через QAtomicInt, QAtomicInteger<T> и std::atomic. В статье разберём, когда они применимы и где их использование избыточно или опасно.
Концепция
Атомарная операция выполняется как единая, неделимая инструкция на уровне процессора — два потока не могут одновременно «увидеть» промежуточное состояние операции. QAtomicInt::fetchAndAddOrdered(), testAndSetOrdered() и аналогичные методы реализуют это для целочисленных операций без необходимости блокировки. В современном C++ предпочтительнее использовать std::atomic<int> напрямую — он более выразителен и стандартен, а QAtomicInt сохраняется в основном для совместимости с более старым кодом и внутренними механизмами Qt (например, реализацией подсчёта ссылок в неявно совместно используемых контейнерах).
Lock-free программирование уместно для очень простых сценариев — счётчики, флаги, простые однопроходные структуры — но быстро становится крайне сложным и подверженным тонким ошибкам при попытке реализовать что-то более сложное, например lock-free очередь общего назначения. Для таких случаев почти всегда лучше использовать проверенные библиотеки или вернуться к мьютексам.
Пример кода
#include <QCoreApplication>
#include <QThread>
#include <QVector>
#include <QDebug>
#include <atomic>
// Атомарный счётчик активных задач без мьютекса
class TaskCounter
{
public:
void taskStarted() { m_activeCount.fetch_add(1, std::memory_order_relaxed); }
void taskFinished() { m_activeCount.fetch_sub(1, std::memory_order_relaxed); }
int activeCount() const { return m_activeCount.load(std::memory_order_relaxed); }
private:
std::atomic<int> m_activeCount{0};
};
// Атомарный флаг для безопасной однократной инициализации без мьютекса
class LazyResource
{
public:
void ensureInitialized()
{
if (m_initialized.load(std::memory_order_acquire)) {
return; // быстрый путь без блокировки в обычном случае
}
bool expected = false;
if (m_initialized.compare_exchange_strong(expected, true, std::memory_order_acq_rel)) {
qDebug() << "Ресурс инициализируется (выполнено ровно один раз)";
// ... тяжёлая инициализация ...
}
}
private:
std::atomic<bool> m_initialized{false};
};
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
TaskCounter counter;
LazyResource resource;
QVector<QThread *> threads;
for (int i = 0; i < 6; ++i) {
auto *thread = QThread::create([&counter, &resource]() {
counter.taskStarted();
resource.ensureInitialized();
QThread::msleep(100);
counter.taskFinished();
});
threads.append(thread);
thread->start();
}
for (auto *thread : threads) {
thread->wait();
delete thread;
}
qDebug() << "Активных задач осталось:" << counter.activeCount();
return 0;
}
Пояснения к коду
TaskCounter использует std::atomic<int> с операциями fetch_add/fetch_sub для безопасного учёта количества активных задач без единого мьютекса — каждая операция атомарна на уровне процессора. LazyResource демонстрирует паттерн однократной ленивой инициализации через compare_exchange_strong: только один из конкурирующих потоков успешно выполнит обмен значения false на true и выполнит инициализацию, остальные просто пройдут мимо.
Использование std::memory_order_relaxed для простого счётчика допустимо, так как порядок видимости отдельных инкрементов друг относительно друга здесь не важен — а для флага инициализации используется более строгий acquire/release, гарантирующий, что эффекты инициализации будут видны всем потокам, которые увидели m_initialized == true.
Подводные камни
- Неверный выбор memory order — одна из самых тонких категорий ошибок в C++.
memory_order_relaxedне подходит, если от порядка операций зависит видимость других, не атомарных, данных в других потоках — в таких случаях нужен как минимумacquire/release, а в сомнительных случаях безопаснее использоватьseq_cst(последовательную консистентность, заданную по умолчанию для упрощённых вызовов без явного memory order). - Lock-free не означает «быстрее» автоматически. При высокой конкуренции (много потоков одновременно пытаются изменить одну и ту же атомарную переменную) накладные расходы на постоянные неудачные попытки CAS могут оказаться выше, чем у простого мьютекса.
- Построение сложных lock-free структур данных «с нуля» (очередей, списков) — задача, в которой даже опытные разработчики совершают ошибки, приводящие к редким, плохо воспроизводимым крашам или повреждению данных. Если задача не сводится к простому счётчику или флагу, разумнее использовать проверенные библиотеки (например, из Boost.Lockfree) или классический мьютекс.
QAtomicIntиstd::atomicрешают только проблему атомарности отдельной операции, но не координацию нескольких связанных операций — если нужно атомарно изменить два значения согласованно, атомарные примитивы по отдельности не дают такой гарантии, и нужен мьютекс или транзакционный подход.