Как absl:: условные критические разделы Mutex обрабатывают пробуждения считывателя

Мне было интересно, где было бы лучше спросить о чем-то подобном, поэтому я задал этот вопрос по Meta (https://meta.stackexchange.com/info/304981) и был направлен здесь, поэтому вот оно.

Мне было очень интересно, какие оптимизации были встроены в реализацию критически важных разделов Google с помощью absl::Mutex (https://github.com/abseil/abseil-cpp/blob/master/absl/synchronization/mutex.h). В частности, мне было интересно, как они обрабатывают пробуждения читателей, когда условие чтения становится истинным. Просыпают ли они всех остальных читателей в списке ожидания? Эта строка, кажется, сигнализирует о том, что они делают. Не рискует ли обход O (n) каждый раз, а также рискует громовыми стадами в записи приоритет мьютекса?

Ответ 1

Я думаю, что важно установить различия между дизайном и оптимизацией реализации. Похоже, что absl:: дает приоритет лучшему дизайну, предоставляя реализацию, которая поддерживает эти проекты, в отличие от предоставления улучшенных оптимизированных компонентов. Иногда лучший дизайн по своей сути лучше работает, чем неуклюже, поэтому результат оптимизируется по обоим факторам (синергия). Absiel.io обсуждает это более подробно.

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

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

В этом суть "оптимизации дизайна": проблема более подробно описана в источнике, и результирующая реализация имеет более высокое качество (производительность, масштабирование,...), чем в противном случае.

Plan9. Переменные условия эквивалентны понятию ядра unix (но не одному) и пробуждению. Когда вы обнаруживаете, что объект не находится в нужном вам состоянии, вы выполняете какое-либо действие, чтобы инициировать его перенос в это состояние, а затем дождаться достижения этого состояния. Например, вы можете искать объект в кеше и, обнаружив его, не существует, создайте недопустимый и попросите его сделать действительным. Это позволяет создать симметрию между всеми участниками кэша: они блокируют объект, оценивают или обновляют его состояние, а затем разблокируют объект и ждут дальнейших изменений.

Уродливая проблема заключается в том, что есть: нет такой вещи, как одновременно, и b: обработка блокировки подвержена ошибкам (я держу любые другие блокировки? может ли это привести к взаимоблокировке?) и не очень хорошо выражено в программе. Plan9s sleep и wakeup элегантно объединяют их, так что в точке, где выполняется условие, никто не может повлиять на объект. Фактически, пункт ссылочного документа заключается в том, как трудно обеспечить выполнение этого простого контракта. Сложные контракты в многопроцессорной среде лучше избегать например.

Критическая оптимизация (в absl:) заключается в том, что нет необходимости проходить через дорогостоящую операцию переключения контекста, чтобы определить, что объект находится в неправильном состоянии, а затем вернуться к сну. Если код, который хочет разбудить вас, может проверить, что состояние объекта - это то, которое вас, скорее всего, интересует, тем лучше. Вызов вызова функции/метода в 100 раз быстрее, чем контекстный.

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

Таким образом, в предыдущем разделе выбираются элементы из официантов списка, и последний раздел пробуждает их. Если вы посмотрите на состояние строки 2203s else, это покажет, как писатель закончит список - w_walk- > wake не верен, и wr_wait был установлен. Кроме того, более ранние тесты в 2015 и 2023 годах предотвращают создание этого списка, если это пробуждение по записи.

мониторы

obj.enter();
while (obj.state != StateIWant) {
    obj.wait();
}
...
obj.exit();

Требуется, чтобы этот поток выполнил условие защиты. Действие перемещения этого заблокированного потока может быть в 1000 раз больше стоимости выполнения условия. Напротив:

obj.wait(^{return obj.state == StateIWant;});
...
obj.exit();

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