пятница, 20 марта 2009 г.

Несколько интересных ссылок

Серьезных, по работе…

Оказывается, есть Multicore Association, которая выпустила спецификацию для Communication API (API для обмена сообщениями на мультиядерных архитектурах). Вот ведь, я даже не слышал никогда…

Не очень серьезных, вокруг работы…

На RSDN есть такой очень знаковый персонаж – Павел Дворкин. Иногда он выдает в форуме “Философия программирования” замечательные посты, которые интересно обсуждать и обдумывать. Вот очередной такой пост: Взгляд со стороны. Имхо, Павел смешивает мелкое с мягким (т.е. приводит некорректное сравнение), но посыл верный: пока в программировании наблюдается очень сильная многостаночность. Мне самому интересно, надолго ли?

Совсем не серьезных, но показывающих, что границы человеческой креативности еще не достигнуты…

Вот как программисты запрещают компьютеру уходить в режим “сна”.

А вот так ремонтируют систему вентиляции в серверной.

Ну а здесь вообще улет: вот до чего могут дойти пастухи овец (найдена у thesz).

SObjectizer4 and Multicore Scalability (век живи – век учись)

В рамках работ над седьмой бетой SObjectizer 4.4.0 потратил несколько дней на то, чтобы выяснить, почему SObjectizer 4.4.0-b6 на тесте customer_ring показывает серьезное падение производительности (результаты можно увидеть здесь).

Дело оказалось в блокировке ядра SObjectizer4, которая выполнялась внутри send_msg для поиска агента-владельца сообщения, поиска экземпляра сообщения и генерации заявок диспетчерам. Блокировка была простая, на основе обычного mutex-а. Поэтому выполнять все эти операции могла только одна нить. Как следствие, на двух ядрах две рабочие нити постоянно конкурировали друг с другом за этот mutex. И пока одна нить занималась отсылкой сообщения, вторая нить ждала первую на входе в send_msg.

В результате я перевел замок ядра SObjectizer4 с обычного mutex-а на reader-writer mutex (с помощью класса ACE_RW_Thread_Mutex). Общая производительность SObjectizer 4.4.0-b7 несколько снизилась по сравнению с 4.4.0-b6. Зато тест customer_ring на двух ядрах показывает увеличение пропускной способности, а не снижение. И, мне кажется, это очень важно.

Вот конкретные цифры для теста customer_ring (на Core2Duo):

Параметры теста

SO 4.4.0-b6

SO 4.4.0-b7

-N 10000 –M 100 395K msg/sec 361K msg/sec
-N 10000 –M 100 –D AG –G 2 172K msg/sec 392K msg/sec
-N 10000 –M 100 –D AG –G 4 250K msg/sec 294K msg/sec
-N 1000 –M 100 –D AO 425K msg/sec 425K msg/sec

Причем ACE_RW_Thread_Mutex, насколько я могу судить, далеко не самый быстрый вариант reader-writer mutex-а (по крайней мере под Windows). На тестах, когда я вместо ACE_RW_Thread_Mutex использовал счетчики на основе ACE_Atomic_Op, получались результаты в районе 600K msg/sec для двух рабочих нитей. Но проверенной реализации эффективного RW_Mutex-а на основе атомарных операций у меня нет, поэтому пока будет задействован ACE_RW_Thread_Mutex. (Я буду признателен, если кто-нибудь укажет мне на OpenSource реализацию быстрого, кроссплатформенного RW_Mutex-а под BSD/MIT-подобными лицензиями. Или даже под GPL-лицензией, чтобы хотя бы идею оттуда можно было взять.)

Блокировка ядра в SObjectizer 4.4.0-b7 в результате усложнилась. Раньше существовало всего два вида блокировок: блокировка для однофазной операции (например, для подписки события агента или отсылки не отложенного и не периодического сообщения) и блокировка для многофазной операции (например, для регистрации кооперации или отсылки отложенного/периодического сообщения). Теперь добавился еще один вид блокировки: для read-only операции (таковой сейчас является отсылка не отложенного и не периодического сообщения). Было бы желательно еще иметь и read-only многофазную операцию. Но я не придумал, как же просто и эффективно ее реализовать :( Поэтому сейчас отсылка отложенного/периодического сообщения выполняется как многофазная операция и эта операция блокирует все остальные операции (т.е. нельзя параллельно отсылать несколько отложенных сообщений). Не самая лучшая ситуация, конечно. Но хотя бы радует то, что отложенные/периодические сообщения отсылаются гораздо реже обычных сообщений.

Какой же вывод из всего вышеизложенного? А вывод очень простой: архитектура SObjectizer4, рассчитанная на использование одного общего системного словаря и адресация агентов через их имена, оказалась не приспособленной к масштабированию на multicore процессорах. Наверное, в рамках SObjectizer4 еще возможны какие-то оптимизации, способные улучшить производительность SObjectizer4 на нескольких ядрах. Но не на порядки.

Что ж, урок хоть и неприятный, но крайне полезный. Это означает, что SObjectizer5 должен строится совсем на других принципах. Наверное, вот так я это представляю себе сейчас:

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

Во-вторых, никаких текстовых имен сообщений. Каждое сообщение, которое можно будет отсылать в SObjectizer, будет представлено своим “доменом” – экземпляром специального типа. Например, есть в программе необходимо сообщения типа msg_do_something, то в программе необходимо будет создать домен для этого сообщения. Например, вот так:

struct msg_do_something : public so_5::message_t
  { ... };
so_5::message_domain_t< msg_do_something > msg_do_something_domain;

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

// Подписка на сообщение.
msg_do_something_domain.subscribe( my_agent_ref, &my_agent_t::evt_do_something );

// Широковещательная отсылка сообщения.
std::auto_ptr< msg_do_something > msg( new msg_do_something( ... ) );
msg_do_something_domain.send( msg );

// Целенаправленная отсылка сообщения.
std::auto_ptr< msg_do_something > msg( new msg_do_something( ... ) );
msg_do_something_domain.send( msg, my_agent_ref );

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

// Если сообщение не отложенное, то нужно сразу отослать его,
// а уже затем разбираться с тем, периодическое ли оно или нет.
// После окончания этой операции ядро гарантированно должно
// быть разблокированно.
if( !delay )
	{
		external_references += deliver_msg_on_blocked_kernel(
				kernel_lock,
				to_deliver,
				insend_dispatching_enabled );
	}
else
	kernel_lock.unlock();

so_4::rt::impl::kernel_t::m_disp->push_delayed_msg(
		timer_msg_data,
		timer_delay,
		period );

Между if-ом и обращением к push_delayed_msg может произойти операция shutdown на другой нити приложения, и тогда указатель kernel_t::m_disp окажется нулевым. Со всеми вытекающими последствиями. Для того, чтобы такой ситуации не происходило в SObjectizer4 есть специальный счетчик многофазных операций и специальное событие, которое выставляется, когда shutdown разрешен.

Поскольку в SObjectizer5 нужно уйти от глобальных счетчиков и блокировок, нужно как-то решить проблему shutdown-а посреди длительной операции. Грубо говоря, чтобы kernel_t::m_disp не обнулялся пока message_domain занимается генерацией заявок.

Может быть, данная проблема вообще не возникнет, если гарантировать, что все ссылки, которыми располагает агент, не могут измениться в течении всей жизни агента. Т.е. пусть будет интерфейс so_5::runtime_t. Программист должен будет создать объект этого типа в самом начале работы приложения. Затем, ссылка на этот объект будет передаваться в конструкторе всем создающимся в программе агентам. Скажем, в базовом классе so_5::agent_t будет единственный конструктор, требующий ссылку на so_5::runtime_t:

class agent_t
	{
	private :
		so_5::runtime_t & runtime_;
	public :
		agent_t( so_5::runtime_t & rt ) : runtime_( rt )
			{}
		...
		so_5::runtime_t & runtime() const { return runtime_; }
	};

После чего везде, где у нас есть ссылка на агента, можно будет безопасно выполнять обращения к runtime:

// Где-то в дебрях отсылки отложенного сообщения...
receiver.runtime().push_delayed_msg( ... );

В таком случае на программисте будет лежать задача обеспечения корректности ссылки на so_5::runtime_t в течении всего времени работы программы. Что, как мне думается, не очень сложно. Зато в ядре SObjectizer, мне кажется, будет проще обеспечить завершение ранее начатых длительных операций при выполнении операции shutdown.

среда, 18 марта 2009 г.

Работать по 10-12 часов в день?

В своей предыдущей заметке я сказал, что когда меня захватывает какая-то идея, то мне проще работать по 10 часов в день, чем следовать обычному 8-ми часовому рабочему дню и пятидневной рабочей неделе. Но нужно обязательно сказать, что далеко не всегда можно плодотворно отработать 10 часов в день. Продолжительность продуктивного рабочего дня сильно зависит от того, чем именно приходится заниматься.

Итак все начинается с этапа “мозгового штурма” или “выкуривания” идеи (от “эту тему нужно покурить”). Как правило, здесь не требуется программировать, зато приходится много думать. Иногда очень много думать. Но я не могу напряженно думать над чем-то более 3-4 часов подряд. Получается, что на стадии обдумывания идеи плодотворный рабочий день у меня длится не более четырех (!) часов. Да, всего четыре часа. Временами еще меньше.

Затем наступает этап воплощения идеи в коде. Здесь так же редко встречаются 10-часовые рабочие дни. Скорее речь идет о 5-6, может быть, 7-ми часах нормального кодирования в день. Тогда пишется хороший код, с комментариями, с тестами, и у меня самого нет претензий к качеству написанного кода.

После того, как код готов, наступает этап документирования. Здесь так же продолжительность плодотворного рабочего дня сильно не дотягивает до стандартных 8 часов. Часов 6 на написание документации в день – это еще можно выдержать, хотя и не очень просто. Иногда бывало и побольше, но впечатления были плохими -- “выжатый лимон” это очень точное выражение.

Так когда же можно плодотворно работать по 10 и более часов? Размышляя над этим я вспомнил три ситуации, когда время летит очень-очень быстро.

Во-первых, можно убить массу времени на проведение различных экспериментов. Например, нужно использовать новую библиотеку в проекте или нужно выяснить, как работать с каким-то устройством. Тут просто не замечаешь, как за написанием небольших тестиков уходит один час за другим. Ведь это же так интересно: а что будет, если вот этот параметр передать? А почему ничего не напечатало? А если вот так? Опять нет! А что Google по этому поводу говорит? Ага, тогда вот так. Опять нет! Ну тогда вот так! Опаньки! А чего же сразу не заработало, я же так в самый первый раз писал! Да, да, я знаю, что уже десятый час, еще минут пятнадцать и поеду домой…

Во-вторых, отладка. В поисках причин различных багов время летит совершенно незаметно. Казалось бы, только недавно пришел на работу, а уже поздний вечер, уже пора идти домой, а хочется попробовать еще вот эту комбинацию входных данных. Поскольку сейчас уже точно получится выловить ошибку! А, не получилось. Значит можно вот так попробовать. Сейчас уже точно получится. Опять не получилось… Ну сейчас-то уж точно… Хорошо, когда такая отладка идет не в ночь перед демонстрацией продукта заказчику. Однажды я убил три дня на поиск чужого бага, когда до сдачи оставалось что-то около недели и это при том, что из-за этой ошибки мы еще ни разу не делали полных тестов всей системы. Я бы тогда сидел на работе и сутки напролет, но предприятие было режимное, и в 22:00 помещение нужно было покинуть…

В-третьих, профилирование и оптимизация. Это занятие сочетает в себе “лучшие” стороны двух предыдущих пожирателей времени. Плюс к тому, в профилировании есть объективная составляющая, очень успешно убивающая время: получение результатов профилирования. У меня как-то была программа, которая под профайлером отрабатывала за пятнадцать минут (ну или около того). Уменьшить объем вычислений нельзя было, т.к. тогда не достигалось нужного количество проходов по проблемным участкам кода. Вот и получалось, что я делал небольшую правку в коде, потом пятнадцать минут ждал результата. Потом еще одну правку и еще пятнадцать минут ожидания. Не удивительно, что в таком режиме рабочие дни сильно превышали 8-мь часов.

Получается, что при нормальном развитии событий продолжительные рабочие дни – это редкость. Сначала “мозговой штурм”, когда работать больше четырех часов не получается физически. Иногда эта стадия разбавляется экспериментами. Тогда несколько дней можно просидеть на работе и по 10 часов. Затем кодирование. Во время которого случаются и периоды отладки, и периоды профилирования. После чего документирование, но там опять работа идет не более 5-6 часов в день.

Важное дополнение. Речь шла именно о времени работы над проектов. А не о времени работы в офисе. Это очень разные вещи. Поэтому, когда мне нужно что-нибудь важное сделать, я стараюсь в офисе не показываться :)

понедельник, 16 марта 2009 г.

По следам прошедшего воскресения

О размеренности…

Выходные – это, пожалуй, самые нелюбимые мои дни недели. Я вообще не очень люблю упорядоченное течение жизни. Эти пять рабочих дней, два выходных, отпуска по расписанию… По темпераменту я спринтер, не стайер. Если загораюсь какой-нибудь идеей, то мне проще вложиться в нее полностью, работать по 10 часов в день, без выходных, чтобы получить результат. И чтобы потом наступила расслабуха – несколько дней, лучше недель ;) полного безделья.

Да, именно такие крайности мне нравятся больше всего: сначала полная концентрация на том, что тебя занимает. Затем полное отключение от всего.

В реальности же есть стандартная рабочая неделя. Проекты, которые нужно сделать (упор на слово “нужно”). Семья, которая не хочет видеть, как я прячусь от ее за письменным столом… Это с одной стороны. С другой стороны есть очередная идея, которая точит мой мозг. Есть желание закрыться где-нибудь часа на три-четыре в одиночестве, с карандашом, бумагой и большой мусорной корзиной. Чтобы раскрутить эту идею с нескольких сторон. Чтобы устать от этого. Чтобы остановиться только тогда, когда я почувствую, что вычерпал тему почти до дна, но только почти, что на донышке еще осталось несколько нетронутых капель. Капель, над которыми затем поработает подсознание…

Но возможности на три-четыре часа отрешиться от окружающего мира нет. Поэтому приходится тратить нервы на то, чтобы отрешится от идеи. Что не очень хорошо получается и это сильно нервирует. Что замето окружающим. Что напрягает меня еще больше, т.к. я понимаю, что тем самым обкрадываю их. Что вытекает в рычание, недовольство, напряжение…

Интересно, изменилась бы ситуация, если бы я занимался более признанной в качестве “творческой” работой? Скажем, был бы писателем или художником. Мог бы я объявить: “У меня вдохновение, я должен зафиксировать этот миг, пока он не исчез”, и закрыться на полдня в мастерской? Возможно, мог бы, поскольку художник может показать дочке фрагмент картины и сказать: “Вот на эту картину я убил сегодняшнее утро. Тебе нравится?”

Про программы такого не скажешь… :(

О фильме “Миллионер из трущоб”

Посмотрел этот фильм находясь под впечатлением рекомендации Гоблина. У меня сложилось несколько негативное отношение к фильму: по мне это голливудская сказка с хэппи-эндом, густо разбавленная “социальной чернухой” для контраста.

Хотя, нужно отдать должное фильму. Опять порадовался, что повезло родится в СССР. И, по крайней мере, в Европейской части СССР не было и нет показанного в фильме ужаса, который до сих пор творится в Индии.

О цифровой фотографии

Цифровой фотоаппарат – это вещь! В течении сорока минут фотографировал увядшую розу. В четыре приема. Сделаю кадров десять-пятнадцать и за компьютер, смотреть, что получилось. Потом еще кадров десять, чтобы снять чуть по другому. И опять посмотреть. И опять переснять. Чтобы выбросить 59-ять неудачных кадров и оставить один, который понравился больше всего:

From Цветы

С пленкой для меня такое было недоступно в принципе!

пятница, 13 марта 2009 г.

Вот так эволюционировал GUI

Интересная статья с подборкой скриншотов об истории эволюции GUI на персональных компьютерах.

Меня всякие рюшечки в виде полупрозрачных иконок и обилия цветовых переходов не интересуют. Да, сначала смотришь на новый интерфейс, иногда даже говоришь “Вау!”. Но буквально через несколько дней перестаешь обращать на оформительские изыски внимания. Поэтому для каждодневной работы для меня нет большой разницы между:

и

Первый даже удобнее (я о KDE 3), поскольку в нем есть одна маленькая, но очень важная фишечка: реализация полос прокрутки. Можно увидеть, что внизу вертикальной полосы прокрутки расположены сразу две кнопочки: вверх и вниз. Что делает очень удобным мелкий скроллинг на строку вверх/вниз. Ведь не приходится перемещать курсор мыши вверх окна. В Windows с этим хуже.

Вот так и оказывается, что не оформление главное, а структура интерфейса (если я правильно выразился).

А вообще лично мне больше всего нравился интерфейс Windows NT 3.51 (которая была до NT 4). Отличная была система: надежная, не требовательная (на 486DX2-80 с 16Mb работала так же здорово, как и OS/2 Warp), простой и удобный интерфейс из Windows 3.*…

Что-то старею я, в ностальгию потянуло…

PS. Готовя этот пост наткнулся на интересный сайтик: http://www.guidebookgallery.org
Откуда и стащил картинку WinNT 3.51.

четверг, 12 марта 2009 г.

Похоже, что SObjectizer 4.4.0 будет поддерживать только TCP/IP

Приступил к разработке седьмой бета-версии SObjectizer 4.4.0. С таким прицелом, чтобы сделать ее сразу релиз-кандидатом. И, если по прошествии трех-четырех месяцев активной эксплуатации не будет выявлено серьезных проблем – объявить ее финальной версией 4.4.0.

Одна из целей beta7 – поддержка еще одного вида транспорта в SObjectizer. Хотел реализовать взаимодействие SObjectizer-процессов через разделяемую память. Благо, видел в ACE средства для ее поддержки. Но не тут-то было. Маленький пушной зверек, как водится, подкрался незаметно.

В ACE действительно есть средства для работы с разделяемой памятью. Низкоуровневые и высокоуровневые. Высокоуровневые (т.к. ACE_MEM_Stream, ACE_MEM_Connector, ACE_MEM_Acceptor) реализованы так, чтобы вписываться в стандартную ACE-овскую архитектуру реакторов. И, поскольку в SObjectizer 4.4 транспортный слой как раз ориентирован на ректоры и Event_Handler-ы, то я решил воспользоваться именно высокоуровневыми средствами.

Разобраться с механизмом работы ACE_MEM_* классов оказалось не просто. Т.к. никакой внятной документации по ним нет, за исключением небольших Doxygen-комментариев. Так что пришлось лазить прямо по исходникам. Здесь в очередной раз хочется сказать, что OpenSource это есть очень хорошо и правильно. При наличии исходников можно понять все. Тем более, что качество кода в ACE довольно паршивенькое, но благо без мозгодробительных трех-этажно-шаблонных конструкций. Разобраться что к чему удалось. И вот, что выяснилось…

Оказывается, у ACE реализован свой транспорт на основе разделяемой памяти. Транспорт этот может быть двух видов: с передачей уведомлений через TCP/IP сокет (режим ACE_MEM_IO::Reactive) или через примитивы синхронизации (режим ACE_MEM_IO::MT). Но в любом случае для канала на основе разделяемой памяти требуется TCP/IP сокет. Насколько я понял, как вся эта кухня работает так:

1. Серверная сторона создает серверный TCP/IP сокет и ожидает подключение клиентов. Когда клиент подключается, серверная сторона через TCP/IP сокет договаривается с клиентом о способе коммуникации (Reactive или MT). После чего создается отображаемый в память файл и имя этого файла передается через тот же сокет клиенту. Клиент получает это имя и открывает данный файл.

2a. Если работа идет в режиме ACE_MEM_IO::Reactive, то о каждой операции записи в разделяемую память делается нотификация удаленной стороны через запись уведомления в TCP/IP сокет. Т.е., при записи в ACE_MEM_Stream данные копируются в разделяемую память, а уведомление о них пишется в сокет.

2b. Если работа идет в режиме ACE_MEM_IO::MT, то используются примитивы синхронизации ОС (вроде бы semaphore и condition variable) для уведомления удаленной стороны. Т.е., при записи в ACE_MEM_Stream данные копируются в разделяемую память и взводится condition variable. Если удаленная сторона спала на этом condition variable, то она проснется.

Подлость в том, что в режиме ACE_MEM_IO::Reactive можно повесить ACE_MEM_Stream на реактор. И реактор будет уведомлять о поступлении данных. Т.е. поддерживается та схема работы, на которую был ориентирован транспортный слой в SObjectizer 4.4.0. Но при этом скорость работы через разделяемую память оказывается (на мелких порциях данных) даже ниже, чем при работе через сокеты. А если взять режим ACE_MEM_IO::MT, то для получения входящих данных нужно висеть на recv() постоянно. Т.е. нужно выделять отдельную нить, которая будет читать входящие данные. А потом еще и управлять этой нитью как-то. Причем хотелось бы, чтобы данная нить могла прослушивать сразу несколько каналов (как это происходит в ACE_Select_Reactor-е с сокетами). И если под Windows еще можно было бы что-то придумать с WaitForMultipleObjects (или ACE_WFMO_Reactor), то что делать под Unix-ами я не очень представляю.

В общем, с этим всем можно было бы бороться, если бы не глюк в ACE, на который мне довелось наткнуться (видно карма у меня плохая, слишком часто глюки в ACE мне попадаются). Глюк сам по себе заслуживающий внимания. Поскольку я даже не придумал, как его исправить и, поэтому, не решил, имеет ли смысл о нем вообще в ace-bugs писать.

Итак, в документации к ACE сказано, что ACE_MEM_Stream за один раз не может передавать больше, чем было первоначально выделено разделяемой памяти. Ну не может, так не может. Но что будет, если попробовать это сделать? Операция recv() возвращает, как положено, –1. А затем программа аварийно завершается. Выяснилось, что деструктор ACE_MEM_Stream пытается записать что-то в канал. Попытка записи приводит к обращению по некорректному указателю. Но откуда этот указатель берется?

Выяснилось, что при попытке записи в память ACE_MEM_Stream просит у подчиненного объекта ACE_Malloc_T подходящий блок. Подходящего блока не находится и в методе ACE_Malloc_T::shared_malloc() выполняется код:

          else if (currp == this->cb_ptr_->freep_)
            {
              // We've wrapped around freelist without finding a
              // block.  Therefore, we need to ask the memory pool for
              // a new chunk of bytes.

              size_t chunk_bytes = 0;

              currp = (MALLOC_HEADER *)
                this->memory_pool_.acquire (nunits * sizeof (MALLOC_HEADER),
                                            chunk_bytes);
              void *remap_addr = this->memory_pool_.base_addr ();
              if (remap_addr != 0)
                this->cb_ptr_ = (ACE_CB *) remap_addr;

Управление попадает в ACE_MMAP_Memory_Pool::acquire(), оттуда в ACE_MMAP_Memory_Pool::map_file(). И одним из первых действий в map_file оказывается:

  // Unmap the existing mapping.
  this->mmap_.unmap ();

т.е. происходит отмена отображения части файла в адресное пространство процесса (соответственно, все адреса, которые были определены в данном отображении, становятся “повисшими”). Нужно это, по-видимому, для того, чтобы затем отобразить в адресное пространство кусок файла большего размера. Но эта попытка завершается неудачно. И, в результате, ACE_MMAP_Memory_Pool::acquire возвращает 0.

Однако, самое важно то, что в ACE_Malloc_T атрибут cb_ptr_ указывает как раз на отображенный в адресное пространство процесса фрагмент файла. Но данного фрагмента уже нет, т.к. было выполнено обращение к unmap()! Т.е. после возврата из acquire() значение this->cb_ptr_ уже содержит мусор!

Похоже, что разработчики ACE расчитывали на то, что после acquire() значение cb_ptr_ станет некорректным. Именно поэтому в коде ACE_Malloc_T::shared_malloc() стоит проверочный код:

              void *remap_addr = this->memory_pool_.base_addr ();
              if (remap_addr != 0)
                this->cb_ptr_ = (ACE_CB *) remap_addr;

Но этот код рассчитан на то, что remap_addr не будет нулевым. Т.е., что map_file() не завершается неудачно. А тут завершается. В результате в this->cp_ptr_ так и остается мусор. И на этот мусор мы натыкаемся, когда деструктор ACE_MEM_Stream пытается что-то записать в канал. Как вполне естественное следствие – крах приложения.

Вот такие вот пироги. Я лично убежден, что раз уж recv() возвращает –1, то ничего больше в программе ломаться не должно. Но в случае с ACE это не так.

По сумме всех вышеизложенных факторов я решил не делать в седьмой бете поддержку транспорта на основе разделяемой памяти. Поскольку высокоуровневый механизм ACE для разделяемой памяти оказался медленным (в случае ACE_MEM_IO::Reactive) или неудобным в использовании (в случае ACE_MEM_IO::MT), да еще и глючным. А делать какой-то свой механизм с нуля не очень хочется. Жалко времени, честно говоря. Есть еще важные вещи, которые хотелось бы включить в SObjectizer 4.4.0 и освободить время и ресурсы на разработку SObjectizer-5.

Такие дела. Так что останется SObjectizer 4.4.0 только с TCP/IP транспортом. По крайней мере пока не возникнет очень настоятельной необходимости в поддержке чего-то другого.

понедельник, 9 марта 2009 г.

Об интуитивности императивного и функционального программирования

Данная тема навеяна очередным RSDN-новским флеймом по поводу функционального программирования (ФП). Толчком стало утверждение, что в 1980-е годы объектно-ориентированное программирование (ООП) с трудом завоевывало себе место под солнцем. Не знаю, что происходило на Западе в 1980-е, но в 1992-м я уже программировал на C++ с объектами, даже не подозревая, что использую ООП. Об этом я узнал несколько позже, наверное, в 1994-м. Тогда же, может чуть раньше, я стал понимать, что переход к ООП действительно требует некоторого изменения способа мышления. Но у меня самого это изменение прошло незаметно и безболезненно. Чего не происходит по отношению к ФП. Почему же?

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

Более того, воспитание человека идет в том ключе, что любое его действие неизбежно ведет к последствиям. Поэтому наши действия должны быть продуманы и взвешены. Чтобы не было мучительно больно и т.д.

Со временем человек учится воздействовать на окружающий мир не только через действия, но и через указания. Начиная с детских требования “хочу конфету”, заканчивая управлением подчиненными в зрелом возрасте. Мы понимаем, что нужно отдавать команды, которые будут приводить к результату. Команды должны быть упорядочены и осмысленны, а это уже программирование. И, что еще важно, команды используются чтобы изменять окружающий нас мир.

Так же в реальной жизни мы встречаемся с модульностью и структурным подходом. Еще в детском саду мы узнаем, что в группе самая главная - воспитательница, но она не самая главная в саду, есть еще и заведующая. Потом мы видим иерархии в школе, в спорте, в армии, на производстве…

Итак, все наше существование показывает нам, что мир вокруг изменяем и, местами, структурирован.

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

Да и само объяснение принципов работы компьютеров, услышанное когда-то в школе, вполне согласовывалось с обычным ощущением изменяемости нашего мира. Мол, есть память, состоящая из ячеек. Есть процессор, есть текущая позиция в памяти. Процессор берет значение из ячейки, текущая позиция сдвигается... И т.д., и т.п. Как-то без проблем стало понятно, почему память конечна, и почему целое число может содержать значения только в каком-то диапазоне. Реальный мир, ничего с ним не поделаешь.

Уже в университете, нам объяснили, что программы бывают не просто большие. А очень-очень большие. Настолько большие, что сам их размер представляет серьезную проблему. И поэтому люди используют по отношениям к программам те же самые приемы, что и по отношению к самим себе - разделяют, властвуют и стараются не выносить сор из избы. Т.е. делят программы на модули и прячут грязное белье этого модуля в нем самом.

Благо, в университете вначале обучение велось на Turbo Pascal, в котором модули были на уровне языка (помнится, они были слизаны с Modula-2). Поэтому вхождение в структурное и модульное программирование произошло быстро и незаметно.

Затем я переключился на C. Хотя сам C мне не нравился. После Turbo Pascal он был какой-то сильно замороченный, да и компилировался на порядки дольше. А осенью 1991-го я раздобыл у знакомого книгу “Язык программирования C++” (первое издание) в электронном виде. По ходу ее первого чтения я даже не отдавал себе отчета о том, что это не C, а другой язык (вероятно, я просто не распечатал “Введение” из книги). На полном серьезе: я полагал, что книга описывает просто новую версию языка C. Поэтому был очень удивлен, когда Turbo C 2.0 оказывался компилировать мои программы с оператором new :)

Так вот, о переходе к ООП. После Паскаля в C мне очень не хватало такой простой и нужной вещи, как множества. В Паскале можно было объявить, например, множество символов и проверять, есть ли там какой-то символ. В C множеств не было, что вызывало у меня жуткий дискомфорт. Зато в Паскале нельзя было написать собственную функцию, получающую переменное число аргументов. Стандартные Write и WriteLn могут получать разное количество аргументов, а мои собственные функции - нет. Т.е. Паскаль не позволял программисту достигать того, что могли сделать разработчики компилятора Паскаль. А в книге я читаю, как в C++ средствами самого языка можно организовать множество. И что это множество будет вести себя точно так же, как и “зашитые” в язык вещи (вроде int). Вот тут-то я и попался на крючок C++. Этот язык давал пользователям языка те же возможности, что и своим создателям.

Само понятие класса в C++ для меня стало аналогом понятия unit из Turbo Pascal. В общем, такое же средство обеспечения модульности, только чуть в других масштабах и с несколько другими возможностями. Кстати, до сих пор очень жалко, что в C++ нет таких модулей, как в Turbo Pascal :(

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

Таким образом, я на собственном опыте убедился, что ООП - это вполне естественная и интуитивно понятная концепция. И что при обучении императивному программированию (т.е. изменению состояния программы через прямые команды) и ООП мы на каждом шагу видим совпадения с тем, что происходит в реальном мире. Именно поэтому обучение императивному программированию идет настолько просто.

В отличие от функционального программирования, которое строится на абстракциях. А абстракции, это такая хитрая штука… Абстракции на то и абстракции, что их сложно объяснить на пальцах, на примерах того, что можно увидеть за окном. В моем случае, проблемы с математикой начались с возникновения в школьной программе пределов, дифференциалов и интегралов. Предел - это абстракция. Для меня предел - это пустой звук. Я не могу себе представить предел ни в виде какого-то предмета, ни в виде рисунка. Поскольку я не могу понять, что это, то я не могу представить себе ни зачем он нужен, ни как его использовать. Выучить вид пределов и операции над ними можно. Но понимания-то нет. А значит, нет и использования.

Так вот, возвращаясь к ФП. Программа, состоящая из функций. А функции не производят побочных эффектов. Мы запускаем функцию несколько раз и получаем один и тот же эффект. Поскольку вокруг ничего не менялось. Т.е. мы нарисовали линию на бумаге. Стерли ее. И нарисовали линию еще раз. Точно такую же. Абсолютно точно такую же. Но ведь это же противоречит тому, что мы познавали в различных проявлениях с самого детства - любое действие изменяет мир вокруг нас. Итак, первое противоречие с практическим опытом: функция - это абстракция, которую сложно объяснить на пальцах.

Далее, программа состоит из функций, которые не производят побочных эффектов. Но в чем смысл таких программ? Программы пишутся для того, чтобы они выдавали результат, т.е. создавали побочные эффекты. Вывод результата на экран - это побочный эффект, ведь содержимое экрана меняется. Второе противоречие: смысл и назначение программы в создании побочных эффектов, но в процессе ее написания мы преследуем другую цель - устранение побочных эффектов.

Более того, поскольку второе противоречие существует объективно, в программах на функциональных языках нужно как-то выделять фрагменты, которые отвечают за производство побочных эффектов. И тут на арену выходят монады. Еще одна абстракция, которую не объяснишь на пальцах…

Что ж, пора закругляться.

Императивное программирование настолько хорошо воспринимается людьми потому, что его принципы согласуется с тем, что человек наблюдает вокруг себя. ООП для меня интуитивно и понятно, поскольку я воспринимал его как продолжение структурности и модульности. А структурность и модульность – естественное следствие борьбы с размером и сложностью.

Функциональное программирование не интуитивно, поскольку его принципы противоречат наблюдаемой мной изменчивости мира. ФП не очень понятно, поскольку оно базируется на абстракциях.

Собственно, в разгорающемся сейчас интересе к ФП меня больше всего волнует вопрос о том, компенсирует ли ФП затраты, понесенные на его изучение и применение. Ответа у меня нет, но есть весьма скептическое отношение к ФП.

Но и яро отрицать ФП я пока не берусь. Поскольку вспоминается аналогия из легкой атлетики. За время существования прыжков в высоту, техника прыжка серьезно менялась два раза. И нынешний прыжок-прогибом, с помощью которого были поставлены современные рекорды, очень далек от интуитивности и очевидности. Может быть, ФП и есть тот самый прыжок-прогибом? Хотя, если продолжать данную аналогию, то более вероятно, что программирование - это вся легкая атлетика, а прыжки в высоту всего лишь одна из ниш в программировании. И ФП будет ставить свои рекорды именно в этой нише.