Простая путаница с многопоточностью для C++

Я разрабатываю приложение C++ в Qt. У меня очень серьезные сомнения, пожалуйста, простите меня, если это слишком глупо...

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

Я спрашиваю об этом, потому что мой ноутбук оснащен процессором i5 3-го поколения (3210m). Так как это двухъядерный и NO_OF_PROCESSORS переменная среды показывает мне 4. Я прочитал в статье, что динамическая память для приложения доступна только для того процессора, который запустил это приложение. Итак, я должен создать только поток 1 (поскольку переменная env говорит о 4 процессорах) или потоки 2 (поскольку мой процессор двухъядерный, а переменная env может указывать на отсутствие ядер) или 4 темы (если эта статья была ошибочной)? Пожалуйста, простите меня, так как я начинающий программист, пытающийся изучить Qt. Благодарю вас :)


person Cool_Coder    schedule 26.01.2013    source источник
comment
У вас есть хорошие ответы здесь. Та статья о динамической памяти, которую вы прочитали, настолько неверна, что мне трудно поверить, что она на самом деле говорит то, что, по вашему мнению, она делает. Есть есть несколько интересных моментов, на которые следует обратить внимание в отношении памяти, распределения памяти и многопоточности. Но на этом уровне не беспокойтесь об этом слишком сильно. И они не означают, что «динамическая память доступна только процессору, запустившему приложение».   -  person Omnifarious    schedule 26.01.2013
comment
Спасибо за ответ!   -  person Cool_Coder    schedule 26.01.2013


Ответы (4)


NO_OF_PROCESSORS показывает 4, потому что ваш процессор поддерживает технологию Hyper-threading. Hyper-threading — это торговая марка Intel для технологии, которая позволяет одному ядру выполнять 2 потока одного и того же приложения более или менее одновременно. Он работает до тех пор, пока, например. один поток извлекает данные, а другой обращается к ALU. Если обоим требуется один и тот же ресурс, а инструкции нельзя переупорядочить, один поток остановится. Вот почему вы видите 4 ядра, хотя у вас их 2.

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

Может помочь больше потоков, чем ЦП, в зависимости от того, как работает планировщик вашей операционной системы / как вы получаете доступ к данным и т. Д. Чтобы обнаружить это, вам нужно будет сравнить свой код. Все остальное будет просто догадками.

Кроме того, если вы пытаетесь изучить Qt, возможно, об этом не стоит беспокоиться...

Изменить:

Отвечая на ваш вопрос: мы не можем сказать вам, насколько медленнее/быстрее будет работать ваша программа, если вы увеличите количество потоков. В зависимости от того, что вы делаете, это будет меняться. Если вы, например. ожидая ответов из сети, вы можете увеличить количество потоков намного больше. Если все ваши потоки используют одно и то же оборудование, 4 потока могут работать не лучше, чем 1. Лучший способ — просто протестировать свой код.

В идеальном мире, если вы «просто» обрабатываете числа, не должно быть разницы, если у вас запущено 4 или 8 потоков, время работы должно быть одинаковым (без учета времени переключения контекста и т. д.). просто время отклика будет отличаться. Дело в том, что нет ничего идеального, у нас есть кеши, ваши процессоры все обращаются к одной и той же памяти по одной и той же шине, поэтому в итоге они конкурируют за доступ к ресурсам. Кроме того, у вас также есть операционная система, которая может или не может планировать поток/процесс в заданное время.

Вы также запросили объяснение накладных расходов на синхронизацию: если все ваши потоки обращаются к одним и тем же структурам данных, вам придется выполнить некоторую блокировку и т. д., чтобы ни один поток не обращался к данным в недопустимом состоянии во время их обновления.

Предположим, у вас есть два потока, которые делают одно и то же:

int sum = 0; // global variable

thread() {
    int i = sum;
    i += 1;
    sum = i;
}

Если вы запустите два потока, делающих это одновременно, вы не сможете надежно предсказать вывод: это может произойти следующим образом:

THREAD A : i = sum; // i = 0
           i += 1;  // i = 1
**context switch**
THREAD B : i = sum; // i = 0
           i += 1;  // i = 1
           sum = i; // sum = 1
**context switch**
THREAD A : sum = i; // sum = 1

В конце концов, sum это 1, а не 2, даже если вы начали тему дважды. Чтобы избежать этого, вы должны синхронизировать доступ к sum, общим данным. Обычно вы делаете это, блокируя доступ к sum столько времени, сколько необходимо. Накладные расходы на синхронизацию — это время, в течение которого потоки будут ждать, пока ресурс снова не будет разблокирован, ничего не делая.

Если у вас есть отдельные рабочие пакеты для каждого потока и нет общих ресурсов, у вас не должно быть накладных расходов на синхронизацию.

person Andreas Wallner    schedule 26.01.2013
comment
Итак, поскольку я могу запускать 4 потока одновременно без каких-либо сбоев, я хотел бы знать, если предположить, что я создал 8 потоков, увеличит ли это время обработки? (при условии, что все потоки обрабатываются одновременно!) Большое спасибо за ответ на простом английском, это действительно помогло :) - person Cool_Coder; 26.01.2013
comment
Я расширил свой первоначальный ответ, чтобы ответить на ваши вопросы. - person Andreas Wallner; 27.01.2013
comment
Спасибо за ответ! Это очень помогло :) - person Cool_Coder; 27.01.2013

Хотя гиперпоточность - это в некотором роде ложь (вам говорят, что у вас 4 ядра, но на самом деле у вас только 2 ядра, и еще два, которые работают только на тех ресурсах, которые первые два не используют, если есть такая вещь), правильнее всего по-прежнему использовать столько потоков, сколько вам говорит NO_OF_PROCESSORS.

Обратите внимание, что Intel не единственный, кто вам лжет, это еще хуже на последних процессорах AMD, где у вас есть 6 якобы «настоящих» ядер, но на самом деле только 4 из них с общими ресурсами между ними.

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

Поэтому, если у вас много работы, и вы можете ее красиво распараллелить, у вас действительно должно быть столько воркеров, сколько разрекламированных ядер (независимо от того, «настоящие» они или «гипер»). Таким образом, вы максимально используете доступные ресурсы процессора.

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

Например, если у вас есть 10 миллионов элементов в массиве для обработки, вы можете отправить задачи, которые ссылаются на 100 000 или 200 000 последовательных элементов (вы не захотите отправить 10 миллионов задач!). Таким образом, вы гарантируете, что в среднем ни одно из ядер не будет простаивать (если одно из них завершится раньше, оно вытянет другую задачу вместо того, чтобы ничего не делать), и у вас будет только сотня или около того синхронизаций, накладные расходы на которые более или менее незначительны.

Если задачи связаны с чтением файлов/сокетов или другими вещами, которые могут блокироваться на неопределенное время, порождение еще 1-2 потоков часто не является ошибкой (требует небольшого количества экспериментов).

person Damon    schedule 26.01.2013
comment
Большое спасибо за эту информацию! Откровенно говоря, я смог понять только небольшую часть того, что вы пытаетесь сказать. Двойное ядро ​​означает, что 1 процессор имеет 2 ядра. Так как мой диспетчер задач показывает 4 графика (по 1 на каждое ядро), то процессоров всего 2 верно? Спасибо еще раз :) - person Cool_Coder; 26.01.2013
comment
Поскольку мое приложение может использовать только 2 ядра, выгодно ли создавать 4 (или более) потока или 2 потока? ПРИМЕЧАНИЕ. Все потоки обрабатываются одновременно! Кроме того, не могли бы вы сказать мне, что вы подразумеваете под накладными расходами на синхронизацию? - person Cool_Coder; 26.01.2013
comment
CAD_Coding: С гиперпоточностью он скажет вам 4 ядра, но их всего два. Количество ядер: на этот вопрос нет простого ответа. 2 или 4 потока более эффективны, зависит от проблемы (например, привязка процессора к вводу-выводу). Я бы просто попробовал и измерил. - person Frank Osterfeld; 27.01.2013
comment
Просто прочитайте статью, в которой объясняется, что мой диспетчер задач показывает 4 графика = 2 для реальных ядер + 2 для гиперядер. Раньше я думал, что раз я вижу 4 графика, то в моем процессоре должно быть 2 процессора (каждый с 2 ​​ядрами!) Вся объединенная информация помогла мне развеять мои сомнения. Спасибо @Damon за реальную и гиперинформацию! Спасибо ФранкОстерфельд тоже :) - person Cool_Coder; 27.01.2013

Это полностью зависит от вашей рабочей нагрузки, если у вас есть рабочая нагрузка, которая очень интенсивно использует процессор, вы должны оставаться ближе к количеству потоков, которые имеет ваш процессор (4 в вашем случае - 2 ядра * 2 для гиперпоточности). Небольшое превышение подписки также может быть приемлемым, так как это может компенсировать время, когда один из ваших потоков ожидает блокировки или чего-то еще.
С другой стороны, если ваше приложение не зависит от процессора и в основном ожидает, вы можете даже создавать больше потоков, чем ваш процессор. Однако вы должны заметить, что создание потока может быть довольно накладным. Единственное решение — измерить, где находится ваше узкое место, и оптимизировать в этом направлении.

Также обратите внимание, что если вы используете С++ 11, вы можете использовать std::thread::hardware_concurrency, чтобы получить переносимый способ определения количества ядер процессора, которые у вас есть.

Что касается вашего вопроса о динамической памяти, вы, должно быть, что-то неправильно поняли. Обычно все потоки, которые вы создаете, могут получить доступ к памяти, которую вы создали в своем приложении. Кроме того, это не имеет ничего общего с C++ и выходит за рамки стандарта C++.

person Stephan Dollberg    schedule 26.01.2013
comment
На самом деле я создаю потоки по мере необходимости, поскольку для одной и той же задачи может потребоваться разное количество операций; поэтому я пытаюсь сформулировать оптимальное отсутствие потоков в зависимости от: 1. количества обработки 2. количества процессоров, которые есть у машины моего клиента. - person Cool_Coder; 26.01.2013

Самый простой способ начать работу с разделением работы между потоками в Qt — использовать среду Qt Concurrent. Пример: у вас есть некоторая операция, которую вы хотите выполнить с каждым элементом в QList (довольно распространенное).

void operation( ItemType & item )
{
  // do work on item, changing it in place
}

QList<ItemType> seq;  // populate your list

// apply operation to every member of seq
QFuture<void> future = QtConcurrent::map( seq, operation );
// if you want to wait until all operations are complete before you move on...
future.waitForFinished();

Qt обрабатывает потоки автоматически... не нужно об этом беспокоиться. Документация QFuture описывает, как вы можете обрабатывать map завершение асимметрично сигналами и слотами, если вам это нужно.

person Jacob Robbins    schedule 26.01.2013
comment
Спасибо за ответ! Я считаю, что обработка потоков с помощью QThread дает мне больше контроля, и я хочу научиться эффективно их использовать. Я обязательно рассмотрю ваше решение и тест для сравнения с QThread. - person Cool_Coder; 27.01.2013