Приведи пример задачи, решаемой с использованием многопоточности в C++.

Ответ

Задача: Параллельное вычисление суммы элементов большого вектора.

Идея: Разделить вектор на несколько частей (по числу потоков), вычислить частичные суммы в каждом потоке параллельно, а затем сложить результаты в главном потоке.

Реализация на C++ с использованием <thread> и <future>:

#include <iostream>
#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <chrono>

// Функция для вычисления частичной суммы в диапазоне [begin, end)
int partial_sum(const std::vector<int>& data, size_t begin, size_t end) {
    return std::accumulate(data.begin() + begin, data.begin() + end, 0);
}

int main() {
    const size_t dataSize = 100'000'000;
    const int numThreads = std::thread::hardware_concurrency(); // Кол-во ядер CPU

    // 1. Генерируем большой вектор данных
    std::vector<int> bigData(dataSize, 1); // Вектор из единиц, сумма = dataSize

    // 2. Делим работу на диапазоны для потоков
    std::vector<std::future<int>> futures;
    size_t chunkSize = dataSize / numThreads;

    auto start = std::chrono::high_resolution_clock::now();

    // 3. Запускаем асинхронные задачи (потоки) через std::async
    for (int i = 0; i < numThreads; ++i) {
        size_t begin = i * chunkSize;
        size_t end = (i == numThreads - 1) ? dataSize : begin + chunkSize;
        // Запускаем вычисление частичной суммы в отдельном потоке
        futures.emplace_back(std::async(std::launch::async, partial_sum, std::ref(bigData), begin, end));
    }

    // 4. Собираем (редуцируем) результаты
    int totalSum = 0;
    for (auto& fut : futures) {
        totalSum += fut.get(); // get() блокирует, пока поток не вернёт результат
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    std::cout << "Total sum: " << totalSum << std::endl;
    std::cout << "Time taken with " << numThreads << " threads: " << duration.count() << " ms" << std::endl;

    // 5. Для сравнения — последовательная версия
    start = std::chrono::high_resolution_clock::now();
    int sequentialSum = std::accumulate(bigData.begin(), bigData.end(), 0);
    end = std::chrono::high_resolution_clock::now();
    duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "Sequential time: " << duration.count() << " ms" << std::endl;

    return 0;
}

Ключевые элементы C++ в примере:

  • std::thread::hardware_concurrency() — получение числа поддерживаемых аппаратно потоков.
  • std::async — высокоуровневый способ запуска асинхронной задачи, возвращающий std::future.
  • std::future — объект для получения результата из асинхронной операции.
  • std::ref() — передача ссылки на вектор в поток (по умолчанию аргументы копируются).

Почему это хороший пример: Задача легко распараллеливается (data parallelism), нет гонок данных (data races), так как каждый поток работает со своим диапазоном, и наглядно виден выигрыш в производительности на многоядерном процессоре.

Ответ 18+ 🔞

А, ну вот, смотри, классическая задачка для демонстрации многопоточности. Типа, есть у тебя вектор, овердохуища размером, и надо всё это богатство просуммировать. Если в лоб, одним потоком — это, конечно, пиздец как долго, как будто на дворе 2002-й год и у тебя одно ядро.

Так, слушай сюда, как мы будем делать по-взрослому. Берём наш здоровенный вектор и режем его на куски, по количеству потоков, которые железо твоё может выдать. Каждый такой кусок — это как отдельная порция работы для своего потока. Потом мы эти потоки запускаем, они там своё дело делают, не мешая друг другу, потому что каждый в своём углу копается. А потом, когда все закончили, мы просто собираем их результаты в кучу — и вот она, общая сумма, готова. Всё просто, как три копейки, но в разы быстрее.

Смотри, вот код, я его чуть поясню, чтобы не было, как говорится, «подозрения ебать чувствую».

#include <iostream>
#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <chrono>

// Функция, которая будет в каждом потоке считать сумму своего куска.
int partial_sum(const std::vector<int>& data, size_t begin, size_t end) {
    return std::accumulate(data.begin() + begin, data.begin() + end, 0);
}

int main() {
    const size_t dataSize = 100'000'000; // Сто лямов элементов, ёпта!
    const int numThreads = std::thread::hardware_concurrency(); // Спрашиваем у системы: "Скока потоков-то дашь?"

    // 1. Набиваем вектор единичками. Сумма должна получиться ровно dataSize.
    std::vector<int> bigData(dataSize, 1);

    // 2. Готовим тару для будущих результатов.
    std::vector<std::future<int>> futures;
    size_t chunkSize = dataSize / numThreads; // Делим поровну на всех.

    auto start = std::chrono::high_resolution_clock::now(); // Засекаем время.

    // 3. Запускаем потоки! Каждому — свой кусок.
    for (int i = 0; i < numThreads; ++i) {
        size_t begin = i * chunkSize;
        size_t end = (i == numThreads - 1) ? dataSize : begin + chunkSize; // Последний поток добирает хвост.
        // Вот эта строчка — магия. std::async запускает задачу асинхронно.
        futures.emplace_back(std::async(std::launch::async, partial_sum, std::ref(bigData), begin, end));
    }

    // 4. Собираем урожай.
    int totalSum = 0;
    for (auto& fut : futures) {
        totalSum += fut.get(); // .get() — это типа "ну давай, отдавай результат, я жду".
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);

    std::cout << "Total sum: " << totalSum << std::endl;
    std::cout << "Time taken with " << numThreads << " threads: " << duration.count() << " ms" << std::endl;

    // 5. А теперь, для полного счастья, посчитаем одним потоком, чтобы понять, насколько мы круты.
    start = std::chrono::high_resolution_clock::now();
    int sequentialSum = std::accumulate(bigData.begin(), bigData.end(), 0);
    end = std::chrono::high_resolution_clock::now();
    duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "Sequential time: " << duration.count() << " ms" << std::endl;

    return 0;
}

Что тут вообще происходит, по пунктам:

  • std::thread::hardware_concurrency() — это типа спросить у компа: «Э, дружище, скока у тебя виртуальных потоков-то?» Обычно это число ядер или в два раза больше.
  • std::async — это просто красота. Ты говоришь: «Хочу запустить вот эту функцию вот с такими аргументами», а система тебе отвечает: «Окей, чувак, делаю», и кидает тебе std::future — это такая расписка, что результат будет.
  • std::future — это и есть та самая расписка. Когда тебе понадобится результат, ты вызываешь fut.get(). Если поток ещё не допилил свои вычисления — главный поток просто уснёт и будет ждать. Никаких гонок, всё цивильно.
  • std::ref(bigData) — важный момент! По умолчанию аргументы в поток копируются. А копировать сто лямов элементов — это, я тебе скажу, терпения ноль ебать. Поэтому мы передаём ссылку, но оборачиваем её в std::ref, чтобы компилятор не испугался.

Почему этот пример — огонь?

Потому что он, блядь, идеально распараллеливается. Каждый поток работает со своим куском памяти, они друг другу не мешают, не пишут в одно место — значит, никаких гонок данных (data races). Это как на заводе: у каждого рабочего свой станок, и они все вместе штампуют детали. А в конце начальник цеха (главный поток) просто всё собирает. На многоядерном процессоре ускорение будет просто ни хуя себе по сравнению с одним потоком. Вот и вся магия, никакой ебалы с мьютексами.