Является ли va_start (и т. д.) реентерабельным?

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

Я не знал о таких проблемах с va_start, так как всегда думал, что это просто макрос для какой-то математики с указателем стека. Есть ли здесь что-то, о чем я не знаю? Я не хочу менять этот код, если будут побочные эффекты.

В частности, рассматриваемая функция выглядит примерно так:

void write(const char *format, ...)
{
    mutex.Lock();
    va_list args;
    va_start(args, format);
    _write(format, args);
    va_end(args);
    mutex.Unlock();
}

Это вызывается из нескольких потоков.


person Nate    schedule 05.10.2010    source источник
comment
Если команда _write блокирует последовательный ввод-вывод на уровне байтов или на уровне буферизации, вы все равно можете захотеть иметь блокировку более высокого уровня, чтобы сделать вызов write() более атомарным. Нет ничего более раздражающего, когда 2 потока вызывают printf("foo") и printf("bar") и получают "fboaor" на выходе вместо "foobar" или "barfoo".   -  person Mark Lakata    schedule 26.10.2012


Ответы (3)


Что касается последовательно-реентерабельности (т. е. если foo() использует va_start, безопасно ли foo() вызывать bar(), который также использует va_start), то все в порядке, пока экземпляр va_list не тот же самый. Стандарт говорит,

Ни макрос va_start, ни макрос va_copy не должны вызываться для повторной инициализации точки доступа без промежуточного вызова макроса va_end для той же точки доступа.

Итак, все в порядке, пока используется другой va_list (упомянутый выше как ap).

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

На более популярных платформах, пока в макрос va_start передается другой аргумент va_list, у вас не должно возникнуть проблем с несколькими потоками, проходящими через «один и тот же» va_start. И поскольку аргумент va_list обычно находится в стеке (и, следовательно, разные потоки будут иметь разные экземпляры), вы обычно имеете дело с разными экземплярами va_list.

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

person Michael Burr    schedule 05.10.2010
comment
О сериализованном вызове — да, здесь есть мьютекс, я просто хотел поместить его на более низкий уровень. Первоначально он был на более низком уровне, но этот коммит 15 лет назад переместил его туда, где вы его видите, утверждая, что мьютекс также необходим для защиты va_list, во что я не верил! - person Nate; 05.10.2010

Ну, то, как доступ к переменным аргументам реализован в C, делает довольно очевидным, что объекты va_list хранят некое внутреннее состояние. Это делает его нереентерабельным, а это означает, что вызов va_start для объекта va_list сделает недействительным эффект предыдущего va_start. Но еще точнее, C явно запрещает повторный вызов va_start для объекта va_list перед "закрытием" ранее вызванного сеанса va_start с va_end.

Объект va_list предполагается использовать "неперекрывающимся" образом: va_start...va_end. После этого вы можете выполнить еще одно va_start на том же va_list объекте. Но попытка перекрыть сеансы va_start...va_end на одном и том же объекте va_list не сработает.

P.S. На самом деле, теоретически, конечно, возможно реализовать некоторое внутреннее состояние на основе LIFO в любом итераторе на основе сеанса. т.е. теоретически возможно разрешить вложенные сеансы va_start...va_end для одного и того же объекта va_list (таким образом делая его повторно входящим в этом смысле). Но спецификация библиотеки C ничего подобного не предусматривает.

Обратите внимание, что в C99 объекты va_list могут быть скопированы va_copy. Таким образом, если вам нужно просмотреть один и тот же список аргументов несколькими перекрывающимися сеансами va_start...va_end, вы всегда можете добиться этого, создав несколько независимых копий исходного va_list.

P.P.S. Глядя на предоставленный вами пример кода... В этом случае нет абсолютно никакой необходимости в каких-либо мьютексах (что касается целостности va_list). И нет необходимости в реентерабельном объекте va_list. Ваш код прекрасно работает без мьютексов. Он будет отлично работать в многопоточной среде. Макросы из группы va_... не работают с фактическим «указателем стека». Вместо этого они работают с полностью независимым va_list объектом, который можно использовать для перебора значений, хранящихся в стеке. Вы можете думать об этом как о своем собственном частном указателе локального стека. Каждый поток, вызывающий вашу функцию, получит свою собственную копию этого va_list, перебирая свой собственный стек. Конфликта между потоками не будет.

person AnT    schedule 05.10.2010
comment
Да, два вызова va_start для одного va_list сделают этот список недействительным. Итак, для статического va_list (или глобального, или члена класса...), возможно, он будет признан недействительным, но будущий вызов той же функции создаст новый va_list. - person Nate; 05.10.2010
comment
@Nate: Да, но не обязательно нужен статический va_list, чтобы столкнуться с проблемой. Подобные проблемы обычно возникают, когда кто-то работает с одним и тем же локальным va_list из одного вызова одной и той же функции с переменным числом переменных. Ничего противозаконного в этом нет. - person AnT; 05.10.2010

На некоторых платформах у va_list могут возникнуть проблемы с повторным входом, но на той же платформе такие проблемы возникают у всех локальных переменных. Мне любопытно, однако: что ожидает ваша функция _write? Если он использует параметры, настроенные перед вызовом записи, это само по себе может вызвать проблемы с потоками, если только (1) какой-либо конкретный экземпляр объекта, содержащий _write, не будет использоваться только одним потоком за раз, или (2) все потоки, использующие объект для _write, потребуют одни и те же параметры настройки.

person supercat    schedule 05.10.2010
comment
_write по-прежнему будет защищен. Я просто хотел снизить защиту до этого уровня и не беспокоиться обо всех указателях va_list. - person Nate; 05.10.2010