Почему я получаю переключение контекста потока каждый раз, когда я синхронизируюсь с мьютексом?

У меня есть несколько потоков, обновляющих один массив в узких циклах. (10 потоков на двухъядерном процессоре при примерно 100 000 обновлений в секунду). Каждый раз массив обновляется под защитой мьютекса (WaitForSingleObject/ReleaseMutex). Я заметил, что ни один поток никогда не выполняет два последовательных обновления массива, что означает, что должен быть какой-то выход, связанный с синхронизацией. Это означает, что каждую секунду происходит около 100 000 переключений контекста, что кажется неоптимальным. Почему это происходит?


person Funky Oordvork    schedule 14.06.2013    source источник
comment
каков приоритет каждого потока?   -  person Kevin MOLCARD    schedule 14.06.2013
comment
Все потоки имеют одинаковый приоритет   -  person Funky Oordvork    schedule 14.06.2013
comment
как именно вы звоните WaitForSingleObject? В частности, какой тайм-аут вы даете ему? Если вы используете INFINITE, я думаю, это будет ожидаемое поведение   -  person Kevin MOLCARD    schedule 14.06.2013
comment
Я использую бесконечность, но я ожидаю, что каждый поток будет использовать свой временной интервал (много последовательных обновлений перед переключением контекста).   -  person Funky Oordvork    schedule 14.06.2013
comment
Возможно это из-за мультипроцессора. Когда первый поток (работающий на первом процессоре) освобождает мьютекс, второй поток (на втором процессоре) его получает, затем, когда первый поток пытается получить мьютекс, он не может. Когда мьютекс окончательно освобождается вторым потоком, он берется третьим потоком (на первом процессоре).   -  person Kevin MOLCARD    schedule 14.06.2013
comment
Вы могли бы быть на что-то там. Итак, просто чтобы уточнить, вы говорите, что это происходит из-за того, что есть 2 физических процессора, и всегда будет поток, ожидающий захвата мьютекса, как только он будет освобожден? Итак, если бы было только одно ядро, то каждый поток выполнялся бы в течение полного временного интервала и выполнял бы много последовательных обновлений?   -  person Funky Oordvork    schedule 14.06.2013
comment
Да, это должно быть так. Вставьте свой последний комментарий в ответ, и я поставлю вам галочку :)   -  person Funky Oordvork    schedule 14.06.2013
comment
Я не пробовал, но почти уверен, что проблема именно в этом. У вас есть доступ к однопроцессорной машине?   -  person Kevin MOLCARD    schedule 14.06.2013
comment
Когда 10 потоков попытаются получить мьютекс, 9 проиграют. Один массив + узкий цикл + много потоков — плохая комбинация, вы не можете добиться параллелизма, если ваш код этого не позволяет. Гугл закон амдаля.   -  person Hans Passant    schedule 14.06.2013
comment
@Hans Passant: я получаю МНОГО параллелизма!   -  person Funky Oordvork    schedule 14.06.2013
comment
@Kevin MOLCARD - извините, я передумал, я не думаю, что это работает несколько физических процессоров :(   -  person Funky Oordvork    schedule 14.06.2013
comment
@FunkyOordvork: нет проблем, по крайней мере, теперь вы будете иметь это в виду :)   -  person Kevin MOLCARD    schedule 14.06.2013


Ответы (2)


Проблема здесь в том, что существует порядок всех ожидающих потоков.

Каждый поток, заблокированный в WaitForSingleObject, помещается в очередь, а затем приостанавливается планировщиком, чтобы больше не отнимать время выполнения. Когда мьютекс освобождается, планировщик возобновляет выполнение одного из ожидающих потоков. Неизвестно, каков точный порядок, в котором потоки пробуждаются из очереди, но во многих случаях это будет простой порядок «первым пришел — первым вышел».

Что происходит сейчас, так это то, что если тот же поток освобождает мьютекс, а затем выполняет другой объект WaitForSingleObject для того же мьютекса, он будет повторно вставлен в очередь, и весьма маловероятно, что он будет вставлен в начало очереди, если уже есть другие потоки ожидания. Это имеет смысл, так как разрешение ему перейти в начало очереди может привести к голоданию других потоков. Таким образом, планировщик, вероятно, просто приостановит его и вместо этого разбудит поток, который находится в начале очереди.

person ComicSansMS    schedule 14.06.2013
comment
Вы говорите, что планировщик запускается каждый раз, когда делается вызов ReleaseMutex? - person Funky Oordvork; 14.06.2013
comment
Не обязательно. Но вашему потоку придется ждать планировщика всякий раз, когда он вызывает WaitForSingleObject, и уже есть другие потоки, ожидающие того же мьютекса. - person ComicSansMS; 14.06.2013
comment
Но ждать планировщика придется только в том случае, если мьютекс занят другим потоком? - person Funky Oordvork; 14.06.2013
comment
Это зависит от реализации. В этом случае системному вызову, инициированному WaitForSingleObject, будет разрешен возврат раньше, но это всего лишь оптимизация. Однако, если он вернется раньше, если есть ожидающие потоки, это приведет к несправедливому планированию. Первый случай — вопрос эффективности, второй — корректности. - person ComicSansMS; 14.06.2013

Думаю, это из-за мультипроцессора.

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

person Kevin MOLCARD    schedule 14.06.2013
comment
Ожидающий поток не должен планироваться вообще в современных операционных системах. Планировщик знает, что поток не может продолжаться, пока мьютекс заблокирован, поэтому он не будет назначать ему временной интервал. Попробуйте сами: вращение this_thread::yield() в C++ всегда приведет к трате всего ядра ЦП, а вызов lock() для мьютекса или wait() для condition_variable — нет. - person ComicSansMS; 14.06.2013
comment
@ComicSansMS: я прочитал ваш ответ и ожидал этого, но не был уверен. Все WaitForSingleObject находятся в очереди, поэтому поток будет после всех других потоков после того, как он вызовет ReleaseMutex. Вот почему я спросил @FunkyOordvork, есть ли у него доступ к одноядерной машине. - person Kevin MOLCARD; 14.06.2013
comment
@ComicSansMS, вы правы, ожидающий поток не запущен, поэтому планировщик должен запускаться каждый раз, когда выходит выпуск, что кажется неоптимальным, но это может быть связано с моим недостатком знаний! - person Funky Oordvork; 14.06.2013
comment
Действительно, было бы интересно проверить, будет ли такое же поведение при использовании только одного ядра. Несколько обращений к SetThreadAffinityMask< /a> должно помочь. - person ComicSansMS; 14.06.2013
comment
@FunkyOordvork Инженер по операционным системам может возразить, что вместо этого вы используете мьютексы неоптимально;) Зачем вообще освобождать мьютекс, если вам нужно сразу же повторно заблокировать его? Вы, вероятно, можете обойти всю проблему, реструктурировав свой код таким образом, чтобы эта ситуация больше не возникала. - person ComicSansMS; 14.06.2013
comment
@ComicSansMS Я думаю, что многие потоки поступают именно так при работе с общими данными. Если я заблокирую и отпущу за пределами своего цикла, а не внутри, тогда другие потоки не заглянут внутрь, и я стану планировщиком. - person Funky Oordvork; 14.06.2013
comment
@ComicSansMS Использование SetThreadAffinityMask не меняет поведение - person Funky Oordvork; 14.06.2013