Я использую занятое ожидание для синхронизации доступа к критическим регионам, например так:
while (p1_flag != T_ID);
/* begin: critical section */
for (int i=0; i<N; i++) {
...
}
/* end: critical section */
p1_flag++;
p1_flag — это глобальная переменная переменная, которая обновляется другим параллельным потоком. На самом деле, у меня есть два критических раздела внутри цикла, и у меня есть два потока (оба выполняют один и тот же цикл), которые коммутируют выполнение этих критических областей. Например, критические области названы A и B.
Thread 1 Thread 2
A
B A
A B
B A
A B
B A
B
Параллельный код выполняется быстрее, чем последовательный, но не так много, как я ожидал. Профилируя параллельную программу с помощью VTune Amplifier, я заметил, что большое количество времени тратится в директивах синхронизации, то есть while(...)
и флаг обновления. Я не уверен, почему я вижу такие большие издержки на этих «инструкциях», поскольку область A точно такая же, как область B. Мое предположение заключается в том, что это связано с задержкой когерентности кэша: я использую Intel i7 Bridge Machine, и эта микроархитектура разрешает когерентность кэша на L3. VTune также говорит, что while (...)
инструкция потребляет всю полосу пропускания внешнего интерфейса, но почему?
Чтобы прояснить вопрос (ы): Почему while(...)
и обновить инструкции флагов, занимающие так много времени выполнения? Почему бы while(...)
инструкция насыщает входную полосу пропускания?
Затраты, которые вы платите, вполне могут быть связаны с передачей переменной синхронизации вперед и назад между основными кэшами.
Когерентность кэша требует, чтобы при изменении строки кэша (p1_flag ++) вам необходимо было владеть ею. Это означает, что он сделает недействительной любую копию, существующую в других ядрах, ожидая, пока он запишет все изменения, сделанные этим другим ядром, на уровень общего кэша. Затем он предоставит линию запрашивающему ядру в M
указать и выполнить модификацию.
Тем не менее, другое ядро к тому времени будет постоянно читать эту строку, читать, что бы отслеживать первое ядро и спрашивать, есть ли у него копия этой строки. Так как первое ядро держит M
копия этой строки будет записана обратно в общий кэш, и ядро потеряет право собственности.
Теперь это зависит от фактической реализации в HW, но если линия была отслежена до того, как изменение действительно было сделано, первое ядро должно было бы попытаться снова получить право собственности на него. В некоторых случаях я думаю, что это может занять несколько итераций попыток.
Если вы настроены на ожидание «занято», вы должны хотя бы использовать некоторую паузу в нем
: _mm_pause
интрис или просто __asm("pause")
, Это одновременно послужит тому, чтобы дать другому потоку возможность получить блокировку и освободить вас от ожидания, а также снизить нагрузку на ЦП при занятом ожидании (неработающий ЦП будет заполнять все конвейеры параллельными экземплярами этого занятого ожидания, потребляя много энергии — пауза будет сериализовать ее, так что в любой момент времени может выполняться только одна итерация — гораздо менее затратная и с тем же эффектом).
Ожидание занятости почти никогда не является хорошей идеей в многопоточных приложениях.
Когда вы заняты ожиданием, алгоритмы планирования потоков не будут знать, что ваш цикл ожидает другого потока, поэтому они должны распределять время так, как будто ваш поток выполняет полезную работу. И процессору требуется время, чтобы проверять эту переменную снова и снова, снова и снова, снова и снова, снова и снова … пока она, наконец, не будет «разблокирована» другим потоком. В то же время, ваш другой поток будет вытесняться вашим занятым ожидающим потоком снова и снова, без всякой цели.
Это еще более серьезная проблема, если планировщик основан на приоритете, а поток ожидания занята с более высоким приоритетом. В этой ситуации поток с более низким приоритетом НИКОГДА не будет вытеснять поток с более высоким приоритетом, поэтому у вас возникнет ситуация взаимоблокировки.
Вы должны ВСЕГДА использовать семафоры или объекты мьютекса или сообщения для синхронизации потоков. Я никогда не видел ситуации, когда ожидание было правильным решением.
Когда вы используете семафор или мьютекс, планировщик знает, что никогда не запланировать этот поток, пока семафор или мьютекс не будет освобожден. Таким образом, ваш поток никогда не будет отнимать время у потоков, которые действительно работают.