Последние потоки выполняются медленнее, чем первые потоки при записи в массив

Я пытаюсь оптимизировать генератор множеств Мандельброта, проблема в том, что я пытаюсь сделать его многопоточным с помощью функции _beginthread(). Компьютерная проблема, которую я решаю, заключается в запуске функции на 2D-плоскости, я пытаюсь запустить около 8 потоков одновременно, каждый из которых вычисляет часть (строку) 2D-массива, но я замечаю, что первые потоки что закончить, закончить намного быстрее, чем последние темы, которые заканчиваются. это вывод:

Starting thread 0
Starting thread 1
Starting thread 2
Starting thread 3
Starting thread 4
Starting thread 5
Starting thread 6
Starting thread 7
Ending thread   0 - Time taken: 1062ms
Ending thread   7 - Time taken: 1031ms
Ending thread   1 - Time taken: 1610ms
Ending thread   6 - Time taken: 1563ms
Ending thread   2 - Time taken: 10265ms
Ending thread   5 - Time taken: 10219ms
Ending thread   4 - Time taken: 31609ms
Ending thread   3 - Time taken: 31641ms

Каждый поток имеет одно и то же, но с разными числами, я не понимаю, почему я получаю такие времена. Вот как я многопоточно:

#define HEIGHT 4000
#define WIDTH 4000
#define MAX_THREADS 8
int const maxIterations = 150;

int bitmap[HEIGHT][WIDTH];
bool finishedThreads[MAX_THREADS];

void renderRow(void * arg) {
    int startTime = GetTickCount();
    int * threadNumPinter = (int*)arg;
    int threadNum = *threadNumPinter;
    int startRow = threadNum * (HEIGHT / MAX_THREADS);
    for (int y = startRow; y <= startRow+(HEIGHT / MAX_THREADS); y++) {
        for (int x = 0; x <= WIDTH; x++) {
            double xx = (((double)x / (double)WIDTH) * 4.0) - 2.0;
            double yy = (((double)y / (double)HEIGHT) * 4.0) - 2.0;
            bitmap[x][y] = isPartOfSet(xx, yy) * 10;
        }
    }
    threadNum = startRow / (HEIGHT / MAX_THREADS);
    finishedThreads[threadNum] = true;
    cout << "Ending thread " << threadNum << " - Time: " << GetTickCount() - startTime << "ms" << endl;
    _endthread();
}


int main() {
    int startTime = GetTickCount();
    HANDLE hThread;
    HANDLE ghEvents[2];
    DWORD dwThreadID;
    int rowsPerThread = HEIGHT / MAX_THREADS;
    int arg;
    int threadIds[MAX_THREADS];
    for (int i = 0; i < MAX_THREADS; i ++) {
        threadIds[i] = i;
        cout << "Starting thread " << i << endl;
        arg = i;
        _beginthread(renderRow, 0, &threadIds[i]);
        Sleep(10);
    }
    bool done = true;//Wait for all threads to finish
    while (1) {
        for (int i = 0; i < MAX_THREADS; i++){
            if (finishedThreads[i] == false)done = false;
        }
        if (done == true) break;
        else done = true;
        Sleep(20);
    }
    saveBitmap(WIDTH, HEIGHT);
    cout << endl << "Rendered in " << double(GetTickCount() - startTime) / 1000.0 << " seconds" << endl;
    cin.get();
    main();
}

Очевидно, что кода больше, чем это, но я не думаю, что это повлияет на проблему. Что я здесь не так делаю? У меня была та же проблема с CUDA, поэтому я считаю, что именно так я реализую многопоточность. Благодарю.

3 ответа

Решение

В своем ответе я не буду затрагивать вопросы потоков / синхронизации или соображений по поводу кэширования - см. Другие ответы / комментарии для этого.

Моя точка зрения другая: вы пишете, что "Каждый поток имеет одно и то же действие, но с разными номерами". Если моя память о наборе Мандельброта мне не помешает, определение того, является ли точка членом набора (IOW, реализация вашей isPartOfSet функция, которую вы не предоставили) является итеративным процессом. Некоторые точки "выручают" быстро, некоторые - нет, и вам нужно продолжать итерации до тех пор, пока вы не определите максимальное число элементов.

Итак, что я говорю: с вашим распараллеливанием "один большой блок на поток", вероятно, естественно, что ваши потоки занимают значительно различное количество времени.

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

Итак, теперь у вас есть M потоков и N блоков работы (с N>>M), и вам нужна реализация, которая позволяет каждому потоку работать в цикле, как

while (worktodo) fetch_a_chunk_of_work_and_do_it ()

Как реализован этот тип модели "производитель / потребитель" - я оставлю это для описания других (или для вас, чтобы гуглить:-))

Классический пример некорректного одновременного использования глобальной переменной.

bool finishedThreads[MAX_THREADS];

является глобальным и доступен из нескольких потоков (записано / прочитано). Вы не можете ожидать, что это сработает. В вашем случае вы даже не должны использовать эту переменную. Вместо этого вам следует ожидать события завершения потока.

Жесткое кодирование до 8 потоков ужасно, а как насчет двухъядерного ноутбука какого-то пользователя? std::thread::hardware_concurrency,

Сон ужасен. Ваша спин-петля - абсолютно НЕ правильный способ сделать это. Извините, просто честно.

использование std::thread и использовать joinждать их завершения. Еще лучше: делайте все, кроме одного рабочего элемента в других потоках, делайте один в главном потоке, затем присоединяйтесь к другим. Если имеется N процессоров, то вы должны создать N-1 потоки и сделать один элемент в главном потоке.

Зачем использовать API-интерфейсы только для Windows, когда классы библиотек C++ существенно лучше?

Предлагаемый способ избежать Sleep

Если простого ожидания выхода из потока недостаточно (с помощью объединения, упомянутого выше), в более сложном сценарии следует использовать std::mutex, std::unique_lock, а также std::condition_variable,

У вас должна быть переменная, которая имеет значение true, когда происходит уведомление. В коде, который ждет, вы получаете мьютекс, проверяете этот флаг, и если он не установлен, вы вызываете wait на условной переменной.

В потоке, который уведомляет другой поток, вы получаете мьютекс, устанавливаете переменную flag, которую я упомянул, используете notify_one или же notify_all метод на условной переменной.

Посмотрите эту ссылку на cppreference. Основные, которые вы используете, это те, о которых я уже упоминал.

Другие вопросы по тегам