суббота, 5 сентября 2009 г.

[comp.prog.cpp] Спецификатор nothow в C++ -- имеет ли это смысл?

Из обсуждения заметки о простоте транзакционного программирования:

jazzer:
Я только добавлю, что у меня всегда в середине таких функций стоит строчка с комментарием
/// no throw after this point

Rubanets Myroslav:
Ну и ценность комментария
/// no throw after this point
у меня вызывает большие сомнения.

jazzer:
насчет ценности комментария - я не знаю другого способа, к сожалению.
Было бы замечательно, если бы можно было объявить блок как nothrow (через атрибуты, скажем), и чтоб компилятор проверял, что я зову только функции, которые тоже nothrow (проверки примерно как const), но такого, увы, нету и не предвидится.

Это обсуждение напомнило мне, что мы с jazzer-ом когда-то на RSDN уже затрагивали тему спецификатора nothrow. Насколько я знаю, он существует сейчас только в одном языке -- D. Да и то, изначально он был просто зарезервированным ключевым словом и никак не обрабатывался компилятором (может сейчас что-то и изменилось, не знаю).

На первый взгляд, такая штука, как nothrow могла бы облегчить программистам жизнь при написании устойчивого к исключениям кода. Например, с ее помощью можно было бы помечать те фрагменты, в которых программист надеется не порождать исключения. Например:

// Реализация оператора копирования для собственного класса.
my_class_t & my_class_t::operator=( const my_class_t & other )
  {
    my_class_t tmp( other );
    // Вот здесь мы гарантируем, что исключений быть не должно.
    nothrow {
      tmp.swap( *this );
    }
  } 

Смысл в том, чтобы компилятор бил по рукам, если в секции nothrow программист пытается выполнять какой-то код, который может порождать исключения. Например, вызывать new. Т.е. программист может использовать внутри nothrow только очень ограниченный набор встроенных инструкций и функции, помеченные спецификатором nothrow. Скажем, функции swap:

// Я уверен, что моя реализация swap не бросает исключений.
void my_class_t::swap( my_class_t & o ) nothrow {
  std::swap( m_a, o.m_a );
  ...
  std::swap( m_b, o.m_b );
} 

На первый взгляд, довольно заманчивая идея. Однако, как водится, дьявол кроется в деталях. (Disclaimer. Поскольку я не являюсь проффесиональным разрабочиком языков программирования и мало что в этом понимаю, то далее будут идти рассуждения о том, в чем я не разбираюсь. В общем, обычная история.)

Во-первых, сразу возникает вопрос: "А можно ли помечать спецификатором nothrow шаблонный код?". Взять, к примеру, тот же std::swap. Ведь он работает по очень простой схеме -- несколько копирований через временную переменную. Операторы копирования могут порождать исключения. Значит, обобщенная (т.е. исходная, шаблонная) версия std::swap не может быть помечена как nothrow. А вот ее конкретные версии-специализации для примитивных типов -- могут. Т.о. получается, что в случае с шаблонами обобщенная версия может не иметь спецификатора nothrow, а специализации -- могут. И наоборот. Получается, что для шаблонов какое-то решение существует. Может некрасивое, но все же.

Во-вторых, существует вопрос о том, насколько nothrow соотносится с системно-зависимыми вещами. Допустим, у нас есть некоторый код:

// Некоторый тип, который мы используем.
struct persistent_t : public ... {
  // Значение этого поля нам потребуется.
  int m_value; 
  ...
};

// Наша операция swap, которая не бросает исключений.
void my_class_t::swap( my_class_t & o ) nothrow {
  ...
}

// Какой-то код, в котором мы хотим получить от компилятора
// контроль за гарантиями безопасности исключений.
void resource_user_t::lock_resource(...) {
  // Вначале захватываем все нужные нам ресурсы.
  persistent_t * p = ...;
  my_class_t tmp_resource = ...;

  // А теперь остается сохранить все это у себя.
  // Но процесс сохранения не должен бросать исключений.
  nothrow {
    std::swap( m_value, p->m_value );
    m_resource.swap( tmp_resource );
  }
} 

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

В первой строке мы делаем swap для двух целочисленных переменных. Откуда здесь взяться проблемам? От хитрых баз данных :) Если мне не изменять память, первые объектно-ориентированные БД, типа Versant и Objectivity, работали через механизм захвата страниц виртуальной памяти. Т.е. когда вы создаете объект в БД, вы получаете на него указатель в своей виртуальной памяти. Но самого объекта в памяти нет. При первом обращении по этому указателю происходит системное исключение, которое перехватывается ран-таймом БД. Он определяет, что нужно загрузить и загружает объект в память. В нашем примере указатель p может как раз указывать на объект в подобной объектной БД. И когда мы попытаемся выполнить swap БД попробует поднять его содержимое из БД. Но это не обязательно увенчается успехом. Результатом чего будет какое-то исключение. В блоке, помеченном, как nothrow.

Во второй строке мы вызываем нашу собственную функцию swap, которая, казалось бы, ничего криминального не делает. Но она может просто не вызваться из-за переполнения стека! Т.е. из-за возникновения системного исключения.

Конечно, в этих случаях будут происходить события, выходящие за юрисдикцию C++ (равно как и в случае с деления на ноль). Но ведь в некоторых ОС восстановление после подобных системных исключений возможно (если мне не обманывает мой склероз, в Windows это делается посредством механизма SEH -- Structured Exception Handling). Т.е., если нам сильно не повезет, то у нас в программе может выжить объект, работа которого была прервана внутри секции nothrow.

Такие вот дела. Наверное, если бы в C++ был nothrow, который не работал бы в экзотических случаях, но зато бил бы разработчиков по рукам в остальных ситуациях, это было бы хорошо. Но, как сказал jazzer, в обозримом будущем в C++ его все равно не планируется. Так что будем посмотреть, что получится у разработчиков D. И получится ли вообще ;)

пятница, 4 сентября 2009 г.

[life.work] Типа бизнес идея -- сайт для фиксации репутации работников

Многие профессиональные сайты (в частности, посвященные программированию) имеют разделы, в которых обсуждаются работодатели. Потенциальные соискатели задают там вопросы "А что за контора XXX?", "А кто работал в YYY?", "А как живется в ZZZ?". На некоторых сайтах (dev.by) даже ведется рейтинг компаний. Чтобы будущие работники могли сориентироваться в ситуации. Но вот я не могу вспомнить, чтобы видел где-нибудь обсуждение соискателей. Чтобы работодатели спрашивали там "А кто знает Иванова Петра Сидоровича, который работал в XXX?", "А как работал Зазнайкин Владимир Семенович в YYY?", "На работу просится Погорелов Роман Юрьевич -- брать или нет?"

Существует такая пословица: "Тридцать лет человек работает на репутацию, затем репутация работает на него". В последние пару недель несколько раз довелось столкнуться с тем, что это не пустой звук. Несколько ребят пролетели мимо работы благодаря заработанной себе репутации. Но у нас город не большой, практически везде есть знакомые или знакомые знакомых, у которых можно узнать, а что за фрукт просится на работу. Однако, в крупных городах это вряд ли возможно. Хотя, думаю, многие наниматели не отказались бы выслушать оценку соискателя от его прошлого работодателя (и желательно не одну).

Так вот идея в том, чтобы предоставить работодателям такую возможность. Создать, скажем сайт staff-reputation.com или reputation.ru, на котором будут публиковаться "рекомендации" для бывших сотрудников. Уходит от вас человек -- вы пишете туда то, что о нем думаете. Приходит к вам соискатель, ищите там то, что о нем написали.

Понятно, что все здесь не так однозначно и есть место для всяческих злоупотреблений. С чем-то, наверное, можно будет справится, с чем-то нет. Но, по крайней мере, появится симметричный ответ :) А то почитаешь рассуждения программистов в форумах о работе -- такие, блин, все крутые и правильные, что спасу нет.

Или такие ресурсы уже есть? А может они уже были, но не выжили?

[comp.prog] О простоте программирования в транзакционном стиле

В заметке, посвященной исключениям, остался без ответа комментарий jazzer-а:

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

Мысль, в общем, правильная. Но дьявол, он же в деталях. И вот сегодня подвернулся хороший пример, демонстрирующий это.

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

 class Sample {
  public :
    void init( const std::string &  resource );
    void perform_action( const std::string & action_data );
  ...
};
 

Используется это все достаточно просто:

 Sample action_performer;
...
while( true ) {
  const std::string resource_name = next_resource();
  action_performer.init( resource_name );

  const std::string data = next_data();
  action_performer.perform_action( data );
}
...
 

Дальше выяснилось, что в процессе работы resource_name очень редко меняются. И можно сэкономить время за счет операций освобождения/захвата ресурсов в методе init. Поэтому, было решено в классе Sample реализовать сохранение имени ресурса, чтобы выполнять переинициализацию только при изменении этого имени. После чего код метода Sample::init() быстренько принял вид:

 void Sample::init(
  const std::string & resource_name )
  {
    if( m_current_resource_name != resource_name )
      {
        m_current_resource_name = resource_name;
        free_resource( m_resource );
        m_resource = alloc_resource();
        if( !m_resource )
          throw ...;
      }
  }
 

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

  • где-то в программе будет создан объект Sample;
  • где-то для него будет вызван метод init в который будет передано, скажем, имя "A". На данный момент это имя не правильное и будет выброшено исключение;
  • исключение будет где-то поймано, будут предприняты какие-то действия по исправлению ситуации (например, ресурс с именем "A" будет создан);
  • где-то опять будет вызван метод init для того же самого объекта Sample с тем же самым именем "A";
  • но этот метод ничего не сделает, т.к. m_current_resource_name будет равно resource_name!

Казалось бы, это детская ошибка. Но ведь программирование как раз и переполнено ошибками, в том числе и детскими. И в этом одна из самых больших сложностей нашего занятия.

Кстати, исправление этой ошибки не так тривиально, как могло бы показаться. Самый первый вариант, который приходит в голову:

 void Sample::init(
  const std::string & resource_name )
  {
    if( m_current_resource_name != resource_name )
      {
        free_resource( m_resource );
        m_resource = alloc_resource();
        if( !m_resource )
          throw ...;
        m_current_resource_name = resource_name;
      }
  }
 

так же ошибочен. И чтобы написать более-менее корректный вариант init, нужно что-то вроде:

 void Sample::init(
  const std::string & resource_name )
  {
    if( m_current_resource_name != resource_name )
      {
        m_current_resource_name.clear();
        free_resource( m_resource );
        m_resource = alloc_resource();
        if( !m_resource )
          throw ...;
        m_current_resource_name = resource_name;
      }
  }
 

Кстати говоря, в этой версии так же есть проблемы, но их обнаружение я оставляю в качестве упраждения читателям ;)

Так что повторюсь еще раз: программирование с исключениями и расчетом на восстановление после исключения требует повышенного внимания от программиста. Даже к мелочам. Или лучше сказать -- особенно к мелочам.

PS. Да, класс Sample мог бы быть спроектирован по другому. Многое из того, с чем мы сталкиваемся могло бы быть сделано гораздо лучше. Но приходится иметь дело с тем, что есть.

[life.politic] Чем русские отличаются от всего остального мира

Из описания истории Натальи Зарубиной в "Блоге разнузданного гуманизма":

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

...По-другому вела себя португальская общественность. Вначале они митинговали и возмущались, писали черт знает что. Но потом поняли, что этим ничего не добьются. 2 муниципалитета (Порту и Барселуша) предложили всей семье Зарубиных (маме-папе -старшей дочери-брату с-семьей) дома. Коммерческая фирма построила кафе, где Наталья смогла бы хозяйничать, а один богатый человек согласился оплатить переезд всей оравы в Португалию. Мало того, они поняли, что в семье все решает бабушка (это она потребовала, чтобы Наталья судилась за дочь) и именно ее пригласили в Португалию выбирать дом и обговаривать условия. Она едет туда в сентябре.

Зачем португальцы делают это? Думаете, у них у всех есть свой бизнес и свои дома, или у них мало других нелегальных эмигрантов с детьми? Они хотят сделать пиар своей стране. Это тот случай, когда на весь мир можно продемонстрировать свое благородство и щедрость, предстать в лучшем свете. И их народ это поддерживает. А у нас готовы были разорвать Наталью и ее родных в клочья: «Пьяницы, быдло! Что им не дай - все пропьют!» И это притом, что ничего о них не знают. Португальцы же решили дать Зарубиным второй шанс, помочь им.

Любят в России мешать с дерьмом своих -- это уж точно.

[life.animals] А белки-то плотоядные!

Вот, например, белка закусывает птичкой:

Найдено здесь.

среда, 2 сентября 2009 г.

[comp.prog] Принцип Fail Fast, Restart Quickly как понимаю его я.

В комментариях к заметке об исключениях я обещал затронуть тему принципа "Fail Fast, Restart Quickly". Вот, нашлось время выполнить это обещание. Неприятно начинать изложение с извинений и отмазок, но я вынужден признать, что не смогу предоставить ссылок на какие-либо документы и статьи, в которых этот принцип был бы описан (тогда как прото по теме Fail Fast информации можно найти довольно много).

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

  • тотальное дублирование;
  • тотальная проверка;
  • быстрый отказ;
  • быстрое восстановление.

Все это присутствует в NonStop-ах как на железном, так и на программном уровне. Например, процессорный модуль (CPU Unit, если не ошибаюсь в терминологии) имеет два одинаковых процессора, два одинаковых банка памяти, две шины памяти, два комплекта портов ввода-вывода и т.д. Все это работает параллельно -- оба процессора выполняют одни и те же инструкции, а память содержит одни и те же данные. Дублирование.

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

После того, как контроллер обнаруживает сбой, он вырубает CPU unit полностью. Быстрый отказ.

О сбойном CPU unit-е становится известно ядру ОС. И ядро предпринимает действия по восстановлению системы. Например, по миграции работавших на этом CPU unit-е приложений на другие CPU unit-ы. Быстрое восстановление.

Аналогичные принципы используются и при построении ПО для NonStop-ов. Программист имеет возможность разрабатывать программы, которые работают в виде пары процессов -- primary- и backup-процессы. Основную работу (например, операции ввода-вывода и пр.) выполняет primary-процесс, а backup-процесс находится в пассивном состоянии. Когда primary-процесс завершает какое-то действие и хочет синхронизировать свое состояние с backup-процессом, то выполняется специальная операция постановки checkpoint-а -- обмен информацией между primary- и backup-процессами. Если primary-процесс после этого упадет или же сбойнет его CPU-unit, то ОС сделает primary-процессом выживший backup-процесс (для чего ОС всегда запускает каждый процесс из пары на разных CPU unit-ах), а сама запустит новый backup-процесс на каком-то другом CPU unit-е.

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

  • как можно более ранее обнаружение неприятностей, и
  • отсуствие каких-либо сложных действий по их преодолению.

Т.е. тот самый Fail Fast.

Если процесс падает из-за каких-то (предположительно временных) проблем, то прикладная система должна продолжать работать. В идеале, сбой вообще не должен быть заметен внешнему миру. Это значит, что сбойный процесс должен как можно быстрее рестартовать и вернуться к работе. Т.е. выполнить Restart Quickly. На NonStop-ах последнее обеспечивается как раз за счет checkpoint-ов, которыми регулярно должны обмениваться между собой primary- и backup-процессы.

Disclaimer. Сразу хочу предупредить. Писать пары процессов для NonStop-ов мне не довелось, т.к. повезло. На NonStop-ах было две программные оболочки. Одна, называемая Guardian, является родной. И все специфические NonStop-овские вещи, включая пары процессов, доступны только под ней. Но программировать под Guardian -- это то еще занятие. Поскольку там своя специфическая файловая система, свой диалект языка C, свой собственный API, свой собственный текстовый редактор, свой собственный shell и т.д. Вторая оболочка, OSS -- Open System Services, это обычный POSIX. Нормальная UNIX-овая файловая система, нормальные C/C++, нормальный POSIX API. Но зато никаких NonStop-овских вкусностей. Я как раз портировал приложение под OSS.

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

Ну например, пусть мы пишем SMTP сервер. Получает он от клиента пару писем, фиксирует их в БД, отсылает клиенту OK, определяет, куда нужно эти письма переслать, выполняет логирование своего намерения... И тут оказывается, что запись в лог не удалась. Не важно почему -- переполнился ли диск, достиг ли файл лога максимального размера, отмонтировали раздел с логами или же он вообще полетел. Не важно. Свое намерение мы не выполнили, значит нужно тушить свет. Т.е. наш SMTP сервер должен завершить свою работу (Fail Fast). Для этого достаточно операций в самом SMTP сервере.

А вот для обеспечения части Restart Quickly нам потребуется дополнительный процесс-монитор (называемый еще watch dog-ом или supervisor-ом). Наш SMTP сервер будет стартовать из-под монитора. И когда монитор обнаружит падение SMTP сервера, он его рестартует. Но это еще не будет полной поддержкой Restart Quickly. Для ее полной поддержки нужно, чтобы SMTP сервер после рестарта вернулся к продолжению операции отсылки тех самых двух писем, на которых все завершилось. Что достигается, например, расстановками каких-либо контрольных точек в БД.

Естественно, принцип Fail Fast извествен очень давно и под разными марками. Так, на мой взгляд, Design By Contract в Eiffel (а так же в D) -- это чистой воды Fail Fast. Причем не только за счет пред- и постусловий. Но еще и за счет инвариантов классов (специальных проверок, которые автоматически запускаются после завершения любого публичного метода класса) и инвариантов циклов. Только вот в Eiffel нет готовой фазы Restart Quickly.

Зато практически все это есть в Erlang. Только там это называется Let It Fail. (Насколько я знаю, в Erlang не принято выполнять проверки пред- и постусловий. Но зато механизм супервизоров и рестартов сбойных процессов там используется на всю катушку).

Отдельной темой в разговоре о принципе Fail Fast является его практическая применимость. Лично мне кажется, что Fail Fast (даже без Restart Quickly) должен использоваться как можно больше. Поскольку он защищает нас от нас же самих. Например, недавно мы запустили суточный тестовый прогон важной программы. Чтобы через сутки обнаружить, что файлы отчетов не были сформированны из-за ошибки в имени каталога в конфигурации. Недоработки в программе -- не проконтролировали результат вызова open, затем писали отчеты в неоткрытый файл без проверки кодов возврата. В результате обнаружили проблему только спустя много часов после ее возникновения. А могли бы сразу.

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

В качестве иллюстрации очень удачного, на мой взгляд, деления приведу программку eMule. У нее есть хитрая особенность -- если по каким-то причинам она не может установить соединение с сервером из файла server.met (список eDonkey-серверов), то имя этого сервера из server.met удаляется. Соответственно, если провайдер на несколько часов "обрубает" канал, то eMule может удалить server.met все имена. Дабы восприпятствовать этому я просто поставил на файл server.met атрибут read-only. Теперь eMule просто пишет, что не может обновить этот файл и все. Ни тебе падений (вполне разумное игнорирование Fail Fast-а, поскольку в многозадачном окружении любой процесс может заблокировать на время файл и это нормально), ни тебе попыток снять read-only.

В качестве иллюстрации неудачного использования Fail Fast: пусть некий сервер получает от клиента пакет команд (индивидуальные команды офомляются в пакет для уменьшения трафика), выполняет их и отсылает клиенту пакет ответов (опять же для экономии трафика). Если при выполнении i-й команды из пакета сервер вдруг обнаруживает, что он не может ее выполнить, то он может отказаться выполнять и все остальные команды из пакета. Якобы руководствуясь принципами Fail Fast. Хотя ошибка может быть вызвана не проблемами сервера, а неправильными параметрами данной команды. И остальные команды могли бы быть успешно выполнены.

К сожалению, каких-то готовых критериев, которые бы подсказали разработчику, где нужно следовать Fail Fast-у, а где лучше его проигнорировать, у меня нет. Тут нужен опыт, вскус, чувство меры, здравый смысл и, наверное, некоторая степень параноидальности в купе с педантичностью. В общем, все то, что со временем приобретает хороший разработчик.

Но вот что касается более конкретных технических деталей, то здесь ситуация получше. Итак, на мой взгляд, для использования принципов Fail Fast необходимо:

  • отказаться от использования языков программирования, в которых нет поддержки исключений;
  • отказаться от использования библиотек, которые не используют исключений (если только речь не идет о суровых real-time или embedded условиях, где исключений нет вообще) (касается D и C++);
  • если уж приходится использовать "старые" библиотеки, которые используют коды возврата или глобальные флаги типа errno, то следует либо проверять коды завершения всех вызовов, либо создавать обертки для используемых функций, которые бы генерировали исключения (касается D и C++);
  • сразу информировать о своих проблемах с помощью исключений;
  • не "проглатывать" исключения. Т.е. блоки catch должны ловить только те исключения, которые вы в данный момент способы обработать или преобразовать в новые исключения. По сути, блок catch должен либо исправлять ситуацию, либо выбрасывать исключение;
  • если это возможно, использовать Design By Contract (пред-, постусловия, инварианты). Как частность -- проверка аргументов в методах/функциях, которые являются внешним итерфейсов ваших модулей/библиотек;
  • писать простой код.

Естественно, это только мой взгляд на вещи и вы не обязаны его разделять. В конце-концов, нормальный C-шный код с assert-ами и abort-ами может быть гораздо надежней переусложненного C++/Java/C# варианта. Но, имхо, на языках с исключениями результат достигается все-таки проще, быстрее и дешевле. Хотя в том же Google в C++ запрещено применять исключения, но Google-разработчикам удается писать отказоустойчивый софт.

Отмечу еще, что по-моему, в языках с поддержкой исключений применять подход Fail Fast, Restart Quickly можно не только на уровне всего приложения, но и на уровне его отдельных модулей. Особенно в безопасных языках, где среда выполнения следит за отсутствием ошибок вроде выхода за пределы массива и перезаписи чужой памяти. Приложение может состоять из нескольких слоев. Сбой на одном из слоев будет приводить к завершению работы на этом слое и передаче описания проблемы (исключения) на более высокий уровень. Где имеется возможность рестартовать сбойный слой полностью. Например, пусть упомянутый выше SMTP сервер состоит из одной главной нити и нескольких рабочих. Рабочие нити выполняют взаимодействие с клиентами и отсылку их почты. Когда рабочий поток обнаруживает сбой (невозможность записать строку в лог-файл), он завершает свою работу и отсылает уведомление главной нити. Та реагирует на это уведомление и стартует новый рабочий поток. Таким образом прицип Fail Fast, Restart Quickly может использоваться на "микроуровне".

Вот, вкратце и все, что я хотел рассказать про Fail Fast и Restart Quickly. Но скажу еще одну вещь, имхо, важную -- это на словах все так просто и гладко. И в тех же рестартах есть очень простая возможность войти в бесконечный цикл постоянных рестартов... Но это уже совсем другая история.

В заключение несколько ссылочек по теме:
Сборник мнений и цитат по поводу Fail Fast, Offensive Programming, Defensive Programming на www.c2.com.
Краткое описание принципов построения и работы NonStop-ов: A NonStop Kernel.
Небольшая и попсовая статья о приципе Fail Fast в контексте C#: Fail Fast.

вторник, 1 сентября 2009 г.

[life] Вот и лето прошло...

Действительно, словно и не бывало. Пожалуй, это лето было самым бездарно загубленным в моей жизни. А какие были надежды! Взять хотя бы один отпуск, съездить куда-нибудь на природу на недельку-другую отдохнуть... Ага, щаз!

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

Не все было так плохо. Удалось жену с дочкой отправить к родственникам отдохнуть. На день здоровья съездить. Написать несколько десятков заметок в блог. Получить интересные комментарии. Возобновить споры в группе SObjectizer-а. Отжаться тридцать раз на брусьях...

В общем, жизнь продолжается. Осталось всего два безнадежных проекта. И неизрасходованные отпуска :)

понедельник, 31 августа 2009 г.

[life] День Блога

В День Блога публикую мои пять блогов:
Lamer's blog (Беларусь)
Million.ша (Израиль)
Блог разнузданного гуманизма (Россия)
Жизнь отраженная в притчах (Узбекистан)
Inspire me, now! (Польша)

PS. Последние пару заметок я написал и опубликовал с помощью интересного редактора блогов Zoundry Raven. Написан он, как я понимаю на C++ и Python. За счет Python-а он стартует не очень быстро и отжирает прилично памяти. Но зато он выкачал все мои предыдущие заметки, показал все ссылки, которые я там сделал, все картинки, на которые я там ссылался. В общем, за счет функциональности некоторую тормознутость и тяжеловесность ему можно простить. Пока он мне нравится гораздо больше Windows Live Writer.

воскресенье, 30 августа 2009 г.

[life.art] Картины на песке

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

[comp.concurrency] Стала доступной информация по Apple-вскому Grand Central Dispatch

Вот: Dispatch Queues (развитие темы).

Если вкратце, то получается, что приложению доступно три типа очередей.

Serial-очереди (они же private dispatch queues) создаются по мере необходимости самим приложением. Serial-очередь выполняет поставленные в нее задачи в том порядке, в котором задачи были помещены в нее. Serial-очередь выполняет только одну задачу за один раз. Поэтому Serial-очереди рекомендуют использовать для организации доступа к одному разделяемому ресурсу.

Concurrent-очереди (они же global dispatch queues) создаются самой ОС для приложения. Они способны выполнять сразу несколько задач из очереди, но на выполнение задачи поступают в том же порядке, в котором ставились в очередь. ОС предоставляет приложению три таких очереди – с малым, нормальным и высоким приоритетом.

Main-dispatch-очередь. Это очередь, которая так же создается самой ОС и связывается с главной нитью приложения.

В очереди ставятся задачи. Каждая задача оформляется в виде блока кода. По сути – лямбда-функции. Для поддержки которых Apple расширила С-шные языки: C, Objective-C, C++ (если не ошибаюсь, раньше этого не было на Mac-ах):

dispatch_queue_t myCustomQueue;
myCustomQueue =
  dispatch_queue_create("com.example.MyCustomQueue", NULL);
 
dispatch_async(myCustomQueue, ^{
    printf("Do some work here.\n");
});

В данном примере блок кода содержится в конструкции ^{…}.

Блоки кода могут ссылаться на переменные, которые находятся вне блока (т.е. лексические замыкания). Те переменные, на которые идут ссылки из блока кода, транслятор оформляет в специальную, размещенную в хипе структуру. Что позволяет обращаться к ним даже после выхода из области видимости, в которой блок был создан. Для примитивных типов, вроде int, это совершенно безопасно. Для сложных типов или динамически-выделяемых ресурсов нужно соблюдать осторожность, чтобы не получить “повисшую” ссылку.

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

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

Есть специальные семафоры (dispatch semaphores), которые позволяют ограничивать количество задач, которые могут быть одновременно запущены для обработки какого-то ресурса (пример в документации был не очень выразителен, поэтому я не полностью просек эту фичу).

Есть даже специальная функция для организации параллельной обработки циклов. Т.е. вместо:

for(int i = 0; i < 100; ++i ) {
  // Какая-то независимая обработка...
}

можно использовать dispatch_apply:

dispatch_apply(100, queue, ^(size_t i) {
  // Какая-то независимая обработка...
});

Вот такой вот лисапед с квадратными колесами. Имхо, подобную штуку можно замутить и на C++0x с лямбда-функциями. А так получилась своего рода “игла для разработчика” – написанный для MacOS код уже никуда не получится портировать, т.к. в коде будут использованы нестандартные расширения языка.

Да, чуть не забыл – под iPhone этот GCD пока не работает.