Приведи пример самой интересной задачи, которую решал.

Ответ

На одном из проектов, связанных с высоконагруженным сервисом обработки финансовых данных, мне нужно было реализовать lock-free очередь для передачи сообщений между потоками. Основная сложность заключалась в обеспечении корректности и производительности при высокой конкуренции на запись и чтение.

Задача: Создать однописатель-многопоточный читатель (SPMC) ring-buffer на C++17 без использования мьютексов.

Решение и ключевые моменты:

template<typename T, size_t Capacity>
class LockFreeSPSCRingBuffer {
    std::atomic<size_t> write_idx_{0};
    std::atomic<size_t> read_idx_{0};
    T buffer_[Capacity];

public:
    bool try_push(const T& item) {
        size_t write_idx = write_idx_.load(std::memory_order_relaxed);
        size_t next_idx = (write_idx + 1) % Capacity;
        // Проверяем, не полна ли очередь, читая индекс чтения
        if (next_idx == read_idx_.load(std::memory_order_acquire)) {
            return false; // Очередь полна
        }
        buffer_[write_idx] = item;
        // Обновляем индекс записи с барьером release, чтобы данные стали видны читателям
        write_idx_.store(next_idx, std::memory_order_release);
        return true;
    }

    bool try_pop(T& item) {
        size_t read_idx = read_idx_.load(std::memory_order_relaxed);
        if (read_idx == write_idx_.load(std::memory_order_acquire)) {
            return false; // Очередь пуста
        }
        item = std::move(buffer_[read_idx]);
        // Обновляем индекс чтения
        read_idx_.store((read_idx + 1) % Capacity, std::memory_order_release);
        return true;
    }
};

Что было интересно:

  1. Глубокое погружение в модель памяти C++: Пришлось тщательно подбирать барьеры памяти (std::memory_order_acquire/release) для гарантии видимости изменений между потоками.
  2. Производительность: Мы сравнивали решение с std::queue + мьютекс и boost::lockfree::spsc_queue. Наша реализация показала сравнимую с boost производительность при 10+ миллионах операций в секунду.
  3. Отладка: Использовали ThreadSanitizer и анализ дампов ядра (core dumps) для поиска тонких гонок данных (data races).
  4. Результат: Очередь стала ключевым компонентом в конвейере обработки, устранив узкое место и снизив задержки на 15%.

Ответ 18+ 🔞

А, слушай, вот тут была история — просто ёперный театр, честное слово. Сижу я значит на одном проекте, где сервис финансовые данные обрабатывал, да с такой нагрузкой, что мама не горюй. Нужно было сделать lock-free очередь, чтобы сообщения между потоками гонять. И главная засада — всё должно было работать чётко и быстро, когда десятки потоков одновременно пишут и читают, как сумасшедшие.

Чего хотели: Сделать кольцевой буфер (ring-buffer) на C++17, где писатель один, а читателей — сколько влезет, и всё это без единого мьютекса, на чистой атомике.

Вот во что это вылилось:

template<typename T, size_t Capacity>
class LockFreeSPSCRingBuffer {
    std::atomic<size_t> write_idx_{0};
    std::atomic<size_t> read_idx_{0};
    T buffer_[Capacity];

public:
    bool try_push(const T& item) {
        size_t write_idx = write_idx_.load(std::memory_order_relaxed);
        size_t next_idx = (write_idx + 1) % Capacity;
        // Проверяем, не полна ли очередь, читая индекс чтения
        if (next_idx == read_idx_.load(std::memory_order_acquire)) {
            return false; // Очередь полна
        }
        buffer_[write_idx] = item;
        // Обновляем индекс записи с барьером release, чтобы данные стали видны читателям
        write_idx_.store(next_idx, std::memory_order_release);
        return true;
    }

    bool try_pop(T& item) {
        size_t read_idx = read_idx_.load(std::memory_order_relaxed);
        if (read_idx == write_idx_.load(std::memory_order_acquire)) {
            return false; // Очередь пуста
        }
        item = std::move(buffer_[read_idx]);
        // Обновляем индекс чтения
        read_idx_.store((read_idx + 1) % Capacity, std::memory_order_release);
        return true;
    }
};

И что тут было интересного, ёпта:

  1. Модель памяти C++: Пришлось в неё влезать по самые уши, бля. Подбирать эти барьеры памяти — acquire и release — чтобы изменения из одного потока вовремя становились видны в другом. А то без этого — доверия ебать ноль, одна гонка данных.
  2. Скорость: Потом сравнивали наше творение с обычной очередью на мьютексе и с boost::lockfree. Наша штука оказалась не хуже бустовской — овердохуища операций в секунду, больше 10 миллионов.
  3. Отладка, мать её: Вот тут было волнение ебать. Ловили тонкие гонки через ThreadSanitizer и разбирали дампы ядра, когда всё падало в пизду. Подозрение ебать чувствую, что где-то косяк, а найти — терпения ноль ебать.
  4. Итог: Эта очередь стала главной шестерёнкой в конвейере. Убрала узкое место, и задержки упали на 15%. В общем, сам от себя охуел, что всё заработало как надо.