Местный
Регистрация: 26.04.2006
Адрес: Удомля, гдежещё
Сообщений: 1,986
Вы сказали Спасибо: 676
Поблагодарили 257 раз(а) в 167 сообщениях
|
Продолжение
Теология ООП (часть II)
19-Nov-2004
Продолжение статьи "Теология ООП"
В этой статье я собираюсь немного попугать вас одной историей о том, как по вине стереотипного объектного мышления в мою программу вкралась весьма неприятная ошибка; далее продемонстрирую пример того, как целый класс можно запросто заменить одной-единственной функцией; и конечно же, как всегда, будет много брюзжания и критики.
Но не сердитесь, мы с вами всего лишь пытаемся найти "срединный путь" в проектировании программ и библиотек.
***
Подобно тому, как всякий естественный язык оттачивается и пополняется наиболее интеллектуальными его носителями - писателями и поэтами, язык математики формируется на кончике пера абстрактно-мыслящей элиты - математиков. И поскольку процесс формирования алгебраической нотации длится уже не один век, этот язык становится все более естественным в том же смысле, что и обычные языки. Естественность в некотором приближении есть эстетика плюс краткость.
Но это так, вообще, о языке математики. Так чем в принципе запись
length(s)
оличается от
s.length()
? Вероятно, принципиальных отличий тут нет за исключением того, что первый способ записи выглядит эстетичнее для тех, кто проходил в школе алгебру и учил ее хорошо. Второй же способ - это некая новая нотация, которая, будучи полностью изолированной (или очищенной - как хотите) от традиционной, приводит к таким странным вещам, как i.add(j). А ведь ОО парадигма поначалу вроде бы пыталась нас убедить в том, что все идет именно к этому: всё есть объекты и методы в них, даже числа и операции над ними.
Но постойте, нотация i.add(j) не является ни эстетичной, ни краткой в сравнении с i+j. Конечно, программирование - это уже не математика (или еще не математика?), и оно имеет право вводить собственные абстракции. (Сами математики, к слову сказать, внеся некоторые дополнения и коррективы в уже имевшийся у них язык, получили Lisp, и весьма счастливы.) Вопрос лишь в том, чтобы вновь вводимые абстракции и нотации были достаточно кратки, выразительны, красивы, и что самое главное - заставляли бы думать человека таким же образом.
Всегда можно распознать в толпе математика по лаконичности и меткости его речи, поскольку внутренний язык у хороших математиков - это по сути математика. Что же дает программисту ООП и какое мышление оно у него формирует? Затрудняюсь сказать, но скорее всего ничего интересного ООП у программиста не формирует. Может быть потому, что эта абстракция еще слишком молода и далека от состояния отточенности и естественности.
Впрочем, перейдем от предположений к конерктным примерам.
***
Существует множество объектных библиотек-оберток для базовых системных сервисов, в том числе и для интерфейсов многопоточного программирования. Обычно сами системные API навязывают нам объектный подход: они как правило создают дескриптор (handle), через который и предполагается манипуляция неким системным объектом, например файлом, графическим элементом или потоком исполнения. Все что остается сделать разработчику "обертки" - это просто инкапсулировать дескриптор внутри класса и затем переписать вызовы системного API в методы. Другими словами, буквально перевести все на язык ООП.
И я тоже рассуждал приблизительно так же, когда описывал классы для многопоточного программирования в PTypes. Класс thread не был исключением, кроме разве что одной тонкости, связанной с запуском потока: дело в том, что системный вызов pthread_create() (или BeginThread() в Windows) не может быть в конструкторе класса, как это часто делается для дескрипторных интерфейсов. Вместо этого, создание дескпритора и запуск асинхронной функции был перемещен в отдельный метод thread::start(). Но дело, собственно, не в этом. Объекты этого класса имеют опцию autofree, которая указывает должен ли объект самоуничтожиться по окончании выполнения асинхронной функции thread::execute().
Так вот, на старых Linux-системах, базирующихся на LinuxThreads, PTypes мог вылетать со странными ошибками порчи памяти (memory corruption). Отладка показала, что LinuxThreads почему-то иногда записывает дескриптор потока в области памяти, в которых когда-то существовал мой объект типа thread, но его в этом месте уже нет. В чем же дело?
Посмотрите на описание pthread_create(). Вы передаете этой функции ссылку на место в памяти, куда система должна записать новый дескриптор потока. По поводу запуска вашей асинхронной функции start_routine не дается никаких гарантий, но вы надеетесь, что она когда-нибудь будет вызвана.
Моя ошибка состояла в том, что асинхронная функция могла быть вызвана и закончена еще до того, как LinuxThreads успевал записать дескриптор в соответствующее поле моего класса. И в случае autofree потока это приводило к порче памяти, поскольку выходило так, что объект удалялся из памяти еще до того, как основной поток вернется из pthread_create(). Интересно, что кроме старых версий Linux ни одна другая система не делала этого в таком порядке, вероятно предвидя потенциальные ошибки вроде моей.
Но если сделать, как говорят киношники, отъезд и взять общий план, то моя ошибка на самом деле происходит от предположения о том, что pthread_create() является конструктором системного объекта. А он таки им не является, или скорее не вписывается в традиционное понятие конструктора, несмотря на внешнюю схожесть. Виной всему - мое стереотипное объектное мышление.
Впрочем, какая-то доля вины тут должна пасть и на разработчиков стандарта POSIX Threads. Размышляя потом над всем этим я также понял, что на самом деле не только pthread_create() не является конструктором, но еще хуже: поток исполнения вообще не является объектом, и следовательно архитекторам POSIX нечего было водить нас за нос своими дескрипторами. Другие функции, использующие дескриптор потока - pthread_join() и pthread_detach() - избыточны и могли быть изъяты из интерфейса (синхронизацию с завершением потока можно реализовать в приложении при помощи семафоров). Если бы создатели PThreads были бы истинными минималистами, то они оставили бы только pthread_create() и уже без того параметра, принимающего дескриптор.
В ядре Linux поддержка потоков долгое время ограничивалась одной-единственной функцией clone(), пока наконец начиная с версии 2.6 публике удалось убедить Линуса Торвальдса ввести полную поддержку POSIX-интерфейса в ядро. Я не в курсе какие при этом приводились доводы, но скорее всего не приводилось ничего путного кроме "POSIX это стандарт". Но стандартный интерфейс может быть таким же несовершенным, как и любой нестандартный, и наоборот. Впрочем, это отдельный разговор.
Конечно же, на уровне приложения иногда полезно описывать классы-обертки для потоков исполнения, но суть вопроса не в этом. Надеюсь уже понятно в чем именно: следует быть осторожнее с ОО-мышлением. И подозреваю, что такая нетривиальная вещь, как потоки исполнения, на самом-то деле оказалась не по зубам объектной парадигме.
***
Другой пример, на сей раз без ужасных ошибок с разрушением памяти: сокеты, и в частности дейтаграммные сокеты. Уже заранее могу вам сказать, что дейтаграммные сокеты не являются объектами, и они по сути притянуты к системному интерфейсу сокетов "за уши" ради единообразия, но из-за этого только вносят путаницу.
В той же самой библиотеке PTypes по пожеланиям пользователей пришлось ввести интерфейс для дейтаграммных сокетов. Недолго раздумывая, я описал два класса ipmessage и ipmsgserver по образу и подобию их поточных эквивалентов ipstream и ipstmserver. (Возможно, для вас непривычен стиль именования в PTypes, но использование этого стиля обосновано: его можно комбинировать с любым другим в одной программе. Если бы PTypes был написан, например, в венгерской нотации, то как минимум один класс программистов - юниксоиды старой школы - были бы жутко недовольны.) В конце концов это всего лишь обертка для системного интерфейса сокетов, и я ничего не пытался менять в этой идеологии.
Сам я никогда не использовал протокол UDP в своих программах, и поэтому развитие этой части библиотеки всегда опиралась на отзывы пользователей. И из этих же самых отзывов я понял, что такой интерфейс для дейтаграмм может легко путать программиста.
Работа с дейтаграммными протоколами сводится к трем основным операциям: посылка пакета, привязка к порту и "слушание" его, получение пакета. Если взять только операцию посылки, то окажется, что для ее осуществления достаточно одной-единственной функции. По сути посылка, если можно так сказать, не имеет состояния, и поэтому нет необходимости описывать класс и размножать объекты; в нашем случае - это класс ipmessage, который таким образом оказывается избыточным. Что касается привязки и получения, то здесь ситуация иная: такой сокет имеет состояние, поскольку система параллельно с вашим приложением должна слушать определенный порт и буферизировать поступающие пакеты. Поэтому, такой класс как ipmsgserver на самом деле нужен, правда даже в нем в принципе есть кое-что лишнее: это методы посылки.
Скорее всего в следующих релизах библиотеки я заменю класс ipmessage на обычную глобальную функцию для посылки дейтаграмм. Класс же ipmessage можно оставить для совместимости и еще для тех, кто со всем этим не разобрался.
***
__________________
I never saw a wildthing sorring for itself.
A small bird will drop frozen dead without ever felt sorry for itself.
|