Всегда ли стоит разделять задачи между потоками? - PullRequest
0 голосов
/ 09 февраля 2020

Раньше я думал, что многопоточность наиболее эффективна, когда мы выполняем блокирующие операции, и в течение этого времени мы можем продолжить выполнение других инструкций в других потоках.

Недавно я выполнил простой тест. Я создал вектор данных и разделил поровну строки между потоками и сравнил время выполнения с одним рабочим потока. Многопоточность была победителем.

Вот мой код:

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

double g_sum = 0;
std::mutex g_mutex;

void worker(const std::vector<double>& vec)
{
    const auto vectorSum = std::accumulate(vec.begin(), vec.end(), 0.0);
    std::lock_guard<std::mutex> lg(g_mutex);
    std::cout << "Thread-Worker adding " << vectorSum << " to final sum ("<< g_sum <<")\n";
    g_sum += vectorSum;
}

int main()
{
    const int ROW_SIZE = 10000000;
    const int threadsSize = std::thread::hardware_concurrency();
    std::cout << "Task will be seprated on " << threadsSize << " threads\n";

    // data vector with row for every thread
    std::vector<std::vector<double>> dataVector;
    double fillVal = 1.1;
    for (auto i = 0; i < threadsSize; ++i, fillVal += 1.1)
    {
        dataVector.push_back(std::vector<double>(ROW_SIZE, fillVal));
    }

    std::vector<std::thread> threadContainer;
    auto start = std::chrono::system_clock::now();
    for (const auto& row : dataVector)
    {
        std::thread thread(&worker, std::ref(row));
        threadContainer.push_back(std::move(thread));
    }
    for (auto& thread : threadContainer)
    {
        thread.join();
    }
    auto end = std::chrono::system_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "threads time: " << elapsed_seconds.count() << "s\n";

    // main thread only
    g_sum = 0;
    start = std::chrono::system_clock::now();
    for (const auto& row : dataVector)
    {
        const auto vectorSum = std::accumulate(row.begin(), row.end(), 0.0);
        std::cout << "Main Thread adding " << vectorSum << " to final sum ("<< g_sum <<")\n";
        g_sum += vectorSum;
    }
    end = std::chrono::system_clock::now();
    elapsed_seconds = end-start;
    std::cout << "one-thread time: " << elapsed_seconds.count() << "s\n";
}

в wandbox (https://wandbox.org/permlink/qah5auBI3ZoAe7B2) с 3 логическими ядрами, результаты синхронизации многопоточности в два раза лучше чем однопоточность.

Мой тест правильный? Могу ли я предположить, что, пропуская дополнительное время реализации, разделение рабочей задачи между потоками всегда лучший выбор?

Ответы [ 2 ]

2 голосов
/ 10 февраля 2020

Параллелизм на основе задач с фиксированным числом потоков (без переподписки) обычно является наиболее эффективным подходом. Однако задачи должны иметь разумный размер, чтобы избежать чрезмерных затрат на планирование. IIR C Как правило, для tbb выполнение задачи должно занимать не менее 10 тыс. Циклов. Одна важная деталь, о которой вы должны быть осторожны, это синхронизация между различными задачами. Поскольку вы обычно не знаете, в каком потоке выполняется задача, вы должны быть осторожны, чтобы не вводить взаимоблокировки (например, вызывая задачу, удерживая блокировку).

Однако, независимо от того, есть проблема или нет может быть эффективно решена с несколькими задачами, в значительной степени зависит от конкретной проблемы c и от того, как она сопоставлена ​​с задачами. Это конечно хорошо работает для вашего примера, но это не может быть обобщено, чтобы быть всегда лучшим выбором .

Просто примечание: я рекомендовал бы использовать существующую структуру планирования задач (например, tbb) вместо того, чтобы катать свой собственный.

0 голосов
/ 10 февраля 2020

Простого ответа нет. Результаты многопоточной синхронизации зависят от реализации. Это может быть быстрее или нет. Есть много тонких мест:

  1. Количество аппаратных потоков. Если вы создаете 100 потоков, когда количество аппаратных потоков равно 1, многопоточное решение будет медленнее, чем однопоточное. Потому что время, затрачиваемое на переключение контекста потоков, будет слишком длинным.
  2. Реализация синхронизации. Например, ведение журнала является медленной работой. Вход в контекст критической секции плох, потому что он медленный. Вы можете ускорить его, если переместите вход в другой поток: std::cout << "Thread-Worker adding " << vectorSum << " to final sum ("<< g_sum <<")\n";. Вы можете поместить в другой поток значения vectorSum и g_sum и зарегистрировать их. Это будет быстрее, чем ожидание операции вывода в заблокированном разделе.
...