#c #multithreading #constructor #infinite-loop
#c #многопоточность #конструктор #бесконечный цикл
Вопрос:
Допустим, у меня есть объект, который предоставляет некоторую функциональность в бесконечном цикле.
Допустимо ли просто помещать бесконечный цикл в конструктор?
Пример:
class Server {
public:
Server() {
for(;;) {
//...
}
}
};
Или в C существует внутренняя проблема инициализации, если конструктор никогда не завершается?
(Идея состоит в том, чтобы запустить сервер, который вы просто говорите Server server;
, возможно, где-то в потоке …)
Комментарии:
1. Я не думаю, что в этом что-то не так , но это, вероятно, будет сбивать с толку пользователей вашего класса. Что плохого в том, чтобы просто поместить цикл в
server::run
функцию или что-то подобное?2. Потому что большинство людей (я полагаю) не ожидают, что конструкторы будут блокировать, не говоря уже о том, чтобы никогда не возвращаться.
3. Я почти уверен, что ваш объект недействителен до тех пор, пока не будет запущен конструктор, поэтому любой доступ к вашему объекту из другого места, скорее всего, будет неопределенным поведением.
4. Разве вы не можете просто написать одну функцию?
5. Технически это нормально, но ИМХО это запах кода. В соответствии с принципом единой ответственности я бы не ожидал
Server server;
создания и запуска сервера.Server server; server.run();
более удобочитаем и удобен в обслуживании.
Ответ №1:
Это не неправильно по стандарту, это просто плохой дизайн.
Конструкторы обычно не блокируют. Их цель — взять необработанный фрагмент памяти и преобразовать его в действительный объект C . Деструкторы делают обратное: они берут действительные объекты C и превращают их обратно в необработанные фрагменты памяти.
Если ваш конструктор блокируется навсегда (акцент на навсегда), он делает что-то другое, а не просто превращает кусок памяти в объект. Можно блокировать на короткое время (мьютекс — прекрасный пример этого), если это служит для построения объекта. В вашем случае похоже, что ваш конструктор принимает и обслуживает клиентов. Это не превращает память в объекты.
Я предлагаю вам разделить конструктор на «реальный» конструктор, который создает серверный объект, и другой start
метод, который обслуживает клиентов (путем запуска цикла событий).
ps: В некоторых случаях вам приходится выполнять функциональность / логику объекта отдельно от конструктора, например, если ваш класс наследует от std::enable_shared_from_this
.
Комментарии:
1. Я с вами в целом, хотя кажется, что современные конструкторы не всегда просто превращают память в объекты. Ваш пример мьютекса является одним из примеров, или другим примером является конструктор потоков C , который немедленно запускает данную функцию. Можно утверждать, что создание сервера — это роль конструктора…
2. Несколько вещей: 1)
std::thread
за этим стоят некоторые плохие решения. вот почемуstd::jthread
был стандартизирован. 2) Я бы предпочел, чтобы объект thread имел метод start 3) Это правда, что иногда мы злоупотребляем нашими собственными принципами. Принципы разработки программного обеспечения — это эмпирические правила — обычно они нам помогают. редко мы нарушаем их, потому что они мешают нам делать что-то более простое и правильное.3. Открытие файла — это то, что вы можете законно сделать в конструкторе RAII; это может блокировать ввод-вывод для поиска имени пути, потенциально десятки миллисекунд или сотни мс в сильно загруженной системе. Или целые секунды, чтобы раскрутить магнитный диск, который простаивал. (Или NFS монтирует сервер на Марсе …)
4. @CaptainCodeman прав: RAII говорит, что конструктор должен быть чем-то большим, чем просто манипулирование памятью.
5. @PaulDraper: акцент на навсегда . Блокировка для получения ресурса сильно отличается от блокировки навсегда .
Ответ №2:
Это разрешено. Но, как и любой другой бесконечный цикл, он должен иметь наблюдаемые побочные эффекты, иначе вы получите неопределенное поведение.
Вызов сетевых функций считается «наблюдаемыми побочными эффектами», так что вы в безопасности. Это правило запрещает только циклы, которые либо буквально ничего не делают, либо просто перетасовывают данные, не взаимодействуя с внешним миром.
Комментарии:
1. Спасибо. Что такое UB?
2. @CaptainCodeman Неопределенное поведение. en.cppreference.com/w/cpp/language/ub Это то, что стандарт называет различным непредсказуемым поведением, которое происходит, когда вы нарушаете определенные правила языка (может случиться все, что угодно, включая код, работающий должным образом, возможно, до тех пор, пока не изменится какая-то несвязанная вещь).
3. @CaptainCodeman — Что каждый программист на C должен знать о неопределенном поведении
Ответ №3:
Это законно, но это хорошая идея, чтобы избежать этого.
Основная проблема заключается в том, что вам следует избегать неожиданных пользователей. Необычно иметь конструктор, который никогда не возвращается, потому что это нелогично. Зачем вам создавать что-то, что вы никогда не сможете использовать? Таким образом, хотя шаблон может работать, это вряд ли будет ожидаемым поведением.
Вторичная проблема заключается в том, что это ограничивает возможности использования вашего серверного класса. Процессы построения и уничтожения C являются фундаментальными для языка, поэтому перехватить их может быть сложно. Например, можно захотеть иметь a Server
, который является членом класса, но теперь этот конструктор всеобъемлющего класса будет блокировать … даже если это не интуитивно понятно. Это также очень затрудняет помещение этих объектов в контейнеры, поскольку для этого может потребоваться выделение многих объектов.
Самое близкое, что я могу придумать к тому, что вы делаете, — это std::thread
. Поток не блокируется навсегда, но у него есть конструктор, который выполняет удивительно большой объем работы. Но если вы посмотрите std::thread
, вы поймете, что, когда дело доходит до многопоточности, удивление является нормой, поэтому у людей меньше проблем с таким выбором. (Я лично не знаю причин запуска потока при построении, но в многопоточности так много угловых случаев, что я не удивлюсь, если он разрешит некоторые из них)
Ответ №4:
Пользователь может ожидать, что ваш Server
объект будет настроен в основном потоке. Затем вызовите server.endless_loop()
функцию в рабочем потоке.
На реальном сервере процесс получения порта требует повышенных привилегий, которые затем могут быть удалены. Или, возможно, у вас есть объект, которому необходимо загрузить настройки. Такого рода задачи могут выполняться в основном потоке до того, как долгосрочный цикл будет выполняться в другом месте.
Лично я бы предпочел, чтобы у вашего объекта была функция «опроса», которая была быстрой и неблокирующей. Тогда у вас могла бы быть функция цикла, которая вызывала poll и sleep в бесконечном цикле. Возможно, у вас даже есть атомарная переменная, которую вы можете установить для выхода из цикла из другого потока. Другой особенностью было бы запустить внутренний поток внутри серверного объекта.
Ответ №5:
Как указывали другие, в этом нет ничего «неправильного» с точки зрения семантики C , но это плохой дизайн. Цель конструктора — создать объект, поэтому, если эта задача никогда не завершится, это будет сюрпризом для пользователей.
Другие внесли предложения относительно разделения этапов построения и выполнения на конструктор и метод, что имеет смысл, если у вас есть другие вещи, которые вы, возможно, захотите сделать с сервером, кроме его запуска, или если вы на самом деле захотите его создать, выполнить другие действия, а затем запустить.
Но если вы ожидаете, что вызывающий объект всегда будет просто делать Server server; server.run()
, тогда, возможно, вам даже не нужен класс — это может быть просто автономная функция run_server()
. Если у вас нет состояния для инкапсуляции и передачи в первую очередь, то вам не обязательно нужны объекты. Автономную функцию можно даже пометить [[noreturn]]
, чтобы и пользователю, и компилятору было ясно, что функция никогда не возвращается.
Трудно сказать, что имеет больше смысла, не зная больше о вашем варианте использования. Но вкратце: конструкторы создают объекты — если вы делаете что-то еще, не используйте их для этого.
Ответ №6:
В большинстве случаев в вашем коде нет проблем. Из-за следующего правила:
Класс считается полностью определенным типом объекта ([basic.types]) (или полным типом) при закрытии } спецификатора класса. В рамках спецификации члена класса класс считается завершенным в телах функций, аргументах по умолчанию, спецификаторах noexcept и инициализаторах членов по умолчанию (включая такие вещи во вложенных классах). В противном случае он считается неполным в пределах его собственной спецификации члена класса.
Однако ограничение для вашего кода заключается в том, что вы не можете использовать glvalue, которое не получается из pointer this
, для доступа к этому объекту из-за неуказанного поведения. Это регулируется этим правилом:
Во время построения объекта, если доступ к значению объекта или любого из его подобъектов осуществляется через glvalue, которое не получено, прямо или косвенно, из указателя конструктора this , значение полученного таким образом объекта или подобъекта не определено.
Более того, вы не можете использовать утилиту shared_ptr
для управления такими объектами класса. В общем, помещать бесконечный цикл в конструктор не очень хорошая идея. При его использовании к объекту будет применяться множество ограничений.