пятница, июля 27, 2007

SysAdmin day

Сегодня последняя пятница июля - SysAdmin Day. C праздником коллеги и друзья!

Ночной дозор, или баллада о системном администраторе.



1.
Опять сгорает за окном
Прошедший день в лучах заката.
И на прощание объята
Лазурь листвы его огнём.

А ты глядишь на свой экран.
Бежит строка, за словом слово,
Мелькают мысли, снова, снова
И разгоняют снов туман.

И ночь вошла в твои покои,
Чуть слышным шепотом берез.
Прохлада ветра, запах роз -
- Мир грёз. Бывает же такое.

А может нет? Там гул машин
Не прекращается веками
И затянуло облаками
Небес мерцающий сатин.

Но это там, а здесь лишь ты
И полусонные мечты.

2.
Мелькают ссылки, адреса
Уже знакомые до боли,
И лишь незримые пароли
Не жгут усталые глаза.

За то их точно помнят руки,
Рождая танец. Каждый миг
Переливается твой
Цветком, незнающим разлуки.

Ты полубог своей системы,
Её и правишь, и кроишь,
А в такт пощелкивает <мышь>,
И разрешаются проблемы.

Бодрящий кофе аромат
Перед экраном монитора.
И мир, в бескрайности простора
Раскрыл пред взором сотни врат.

Но кто же ты? Узнать изволь,
Твоей души логин, пароль.

3.
Зачем сидишь, скажи на милость?
Неужто ночью не до сна?
И кофе выпито до дна.
И голова почти склонилась:

Но как бы не слипались веки.
В лучах ЭЛТ, или ЖК
Ты <улетаешь> на века
И забываешься на веки

В кибернетической цепи,
Среди мерцания диодов,
Лавины импульсов и кодов,
Перекликания .

И вот, с невидимой рекой
Поток проходит через <шлюзы>.
объединяет узы
Кибернетической рукой.

Сквозь магистралей волокно
Летит <привет> в твоё <окно>.

4.
Природа спит, во мраке ночи.
Горят рядами фонари.
Всё дожидается зори.
И ты зеваешь, между прочим.

А что в сети? Там тоже штиль.
И, на глазах редеет <чат>.
<Порты свободные> молчат.
И тишина на сотни миль.

Невозмутимый океан
Всерастворяющей тиши
Уже наполнил мир души
И потушил цветной экран.

Но лишь струится в кулера,
Машин спокойное дыханье,
И электронное сознанье
Еще хранят процессора.

Огромный безмятежный мир
Пронзает радиоэфир.


c Copyright: Вьюга(Z-admin), 2005

Ключевой сотрудник.

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

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

Кто-то считает, что зарплата должна расти автоматически, с ростом рыночного индекса. Другие думают, что зарплату надо поднимать в зависимости от уровня знаний. Прочитал книжек, получил сертификат, изучил новую технологию – получи прибавку к зарплате.

Поднимать зарплату за чтение умных книжек? За полученный сертификат? Дудки!!! Это вам скажет любой работодатель.

Зарплату поднимают за степень полезности - важности сотрудника.

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

Есть такое понятие - ключевой сотрудник. Оно никак не соотносится с другим похожим словосочетанием "незаменимый сотрудник", и с тем хорошо или плохо поставлены процессы разработки. Просто есть люди, которые на своем месте приносят гораздо больше пользы, чем обычный среднестатистический работник. Обычно это не тот, кто знает больше всех технологий, и прочел больше всех книжек. Более важна ориентация на результат, инициатива и надежность человека. Хотите прибавки? Поднимайтесь над средним уровнем, берите ответственность на себя. Сделайтесь ключевым сотрудником. Это более надежный путь к финансовому благополучию, чем получение сертификатов.

Есть правда, такой тип руководителей (и контор) которые дают тебе работать (если до работы жадный), а платить за это не любят. Приходишь на новое место, зарплата небольшая но, как говорится, с перспективой роста. Отработал год, результаты отличные. Все тебе говорят:"Молодец, Федя, так держать!", а в расчетном листке это никак не отражается. Тут конечно, без вопросов, новую работу надо подыскивать.

четверг, июля 26, 2007

Программное обеспечение и параллельная революция (Часть 5)

Херб Саттер (Herb Sutter) и Джэймс Лэрус (James Larus), Microsoft.

Часть 5. Потребности в инструментах

Что нам надо среди инструментов

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

Параллелизм порождает новые типы программных ошибок, в дополнение к тем, что хорошо знакомы нам по обычному программированию. Гонки данных (data races (в результате недостаточной синхронизации и клинчей), активные тупики (livelocks) ( в результате излишней синхронизации) представляют собой дефекты, сложные для обнаружения и понимания, поскольку их поведение неопределенно и трудно воспроизводимо. Обычные методы отладки, такие как повторное исполнение с предварительно установленными точками останова не работают для параллельных программ, чьи пути исполнения и поведение могут изменяться от прогона к прогону.

Инструменты для систематического выявления дефектов необычайно важны в этом мире. Такие инструменты используют статический анализ кода программы для систематического выявления всех возможных путей исполнения, таким образом, они способны обнаружить ошибки, которые невозможно воспроизвести. Хотя подобные методики, такие как верификация модели, с большим успехом используются для обнаружения дефектов в аппаратном обеспечении, которому тоже присущ параллелизм, с программным обеспечением все сложнее. Количество возможных состояний любой программы гораздо больше, чем это характерно для большинства hardware, поэтому необходимо гораздо больше работы для систематического анализа состояний программы. В обоих случаях, модульность и абстракция являются ключевыми моментами для упрощения анализа. Если при тестировании модели процессора мы можем выделить АЛУ (арифметико-логическое устройство) и анализировать его отдельно от набора регистров, то задача анализа существенно упрощается.

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

Инструменты для выявления дефектов в параллельном ПО являют собой область для активных исследований. Одна из перспективных методик от Microsoft Research под названием KISS (Keep it Strictly Sequential) (4) преобразует многопоточную программу в последовательную, которая включает все возможные чередования оригинальных потоков. Преобразованная таким образом программа может быть проанализирована большим количеством существующих инструментов для анализа последовательных программ.

Даже продвинутые программисты все же нуждаются в хороших отладчиках, которые позволили бы им разобраться в запутанных и сложных для воспроизведения взаимодействиях в их параллельных программах. Существует две основные методики для сбора этой информации. Первая, это улучшение возможностей логирования, включая отслеживание сообщений передаваемых процессам и того, какие потоки обращаются к каким данным, что позволило бы разработчику отследить и понять порядок исполнения в своей программе. Разработчики также нуждаются в возможностях отслеживать причинно следственные связи между потоками (например, какие сообщения направлены одному активному объекту, когда обработаны, как связаны с другими сообщениями к другим активным объектам?), перезаписи и изменения порядка сообщений в очередях, пошагового исполнения асинхронных вызовов включая обратные вызовы (callbackы), и прочие возможности исследования параллельного исполнения своих программ. Другой подход, это обратное исполнение кода, которое позволяет программисту записать ход исполнения программы и затем повторно «проиграть» этот код. Replay debugging не является новой идеей, но ее реализации мешали высокая сложность и ресурсоемкость. Появившиеся недавно мониторы виртуальных машин (5) (virtual machine monitors) способны снизить оба фактора. В параллельном мире эта методика, вероятно, окажется востребованной.

Нагрузочная отладка и оптимизация также потребуют новых инструментов в параллельном мире. Параллелизм порождает новые узкие места, такие как конфликты блокировок, издержки на поддержание когерентности кэша (проблема false sharing, например. Примечание переводчика), “lock convoys”, которые трудно отследить при помощи обычных профилировщиков. Новые инструменты, более приспособленные к конкретным компьютерным архитектурам и к особенностям параллельных программ, будут более эффективны при обнаружении подобных проблем.

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

Ключ в параллелизме

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

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

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

Ссылки

1. Sutter, H. 2005. The free lunch is over: a fundamental turn toward concurrency in software. Dr. Dobb’s Journal 30 (3); http://www.gotw.ca/publications/concurrency-ddj.htm.
2. Ramalingam, G. 2000. Context-sensitive synchronization-sensitive analysis is undecidable. ACM Transactions on Programming Languages and Systems 22 (2): 416-430.
3. Dean, J., and Ghemawat, S. 2004. MapReduce: simplified data processing on large clusters. Proceedings of the Sixth Symposium on Operating Systems Design and Implementation, San Francisco, CA: 137-150.
4. Qadeer, S., and Wu, D. 2004. KISS: Keep it Simple and Sequential. Proceedings of the ACM SIGPLAN 2004 Conference on Programming Language Design and Implementation, Washington, DC: 1-13.
5. King, S. T., Dunlap, G. W., and Chen, P. M. 2005. Debugging operating systems with time-traveling virtual machines. Proceedings of the 2005 Annual Usenix Technical Conference, Anaheim, CA: 1-15.
6. Chandra, S., Godefroid, P., and Palm, C. 2002. Software model checking in practice: an industrial case study. Proceedings of the 24th International Conference on Software Engineering, Orlando, FL: 431-441.

Программное обеспечение и параллельная революция (Часть 4)

Херб Саттер (Herb Sutter) и Джэймс Лэрус (James Larus), Microsoft.

Часть 4. Потребности в языках

Что нам надо от программных языков

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

Явная, неявная и автоматическая параллелизация

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

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

Другой широко обсуждаемый подход, это автоматическое распараллеливание, когда компилятор пытается обнаружить параллелизм в программах, написанных на обычных языках, таких как ФОРТРАН. Несмотря на то, что данный подход выглядит очень привлекательно, на практике он работает не так хорошо, как ожидалось. Для понимания возможного поведения программы необходим точный анализ. Этот анализ достаточно сложен для таких простых языков как ФОРТРАН, и на порядки сложнее для таких языков как С, манипулирующих структурами данных на указателях. Кроме того, последовательные программы обычно используют последовательные алгоритмы и содержат мало параллелизма.

Императивные и функциональные языки

Популярные коммерческие языки программирования (такие как, Pascal, C, C++, Java, C#) являются императивными языками, в которых программист шаг за шагом описывает изменения, производимые над переменными и структурами данных. Мелкозернистые управляющие конструкции (такие как циклы), низкоуровневое манипулирование данными, разделяемые, изменяемые (shared mutable) экземпляры объектов делают программы на таких языках сложными для автоматического распараллеливания.

Существует популярная точка зрения, что функциональные языки, такие как Scheme, ML или Haskell могли бы устранить эти препятствия, потому что они естественным образом подходят для параллельного исполнения. Программы, написанные на этих языках, манипулируют неизменяемыми (immutable) экземплярами объектов, которые не подвержены угрозам конкурентного доступа. Кроме того, в отсутствии побочных эффектов (Примечание переводчика, side effects – эффекты изменения глобальных переменных после выполнения подпрограммы.) кажется, что программа имеет меньше ограничений на порядок исполнения.

Однако на практике, функциональные языки не обязательно способствуют параллелизму. В программах на функциональных языках, параллелизм обычно возникает на уроне вызовов процедур, что не очень удобно для обычных параллельных процессоров. Коме того, большинство функциональных языков допускают наличие побочных эффектов (side effects) в изменяемых данных (mutable state), и код, использующий эти возможности, весьма сложен для автоматического распараллеливания.

Эти языки вводят понятие «изменяемого состояния» (mutable state) в целях выразительности и эффективности. В чисто функциональном языке, составные структуры данных, такие как массивы или деревья, обновляются путем создания копии, содержащей измененное значение. Эта техника привлекательна с точки зрения семантики, но может быть ужасна с точки зрения производительности (линейный алгоритм с легкостью превращается в квадратичный). Также, функциональные языки никак не препятствуют написанию чисто последовательных алгоритмов, где каждая операция ожидает, пока предыдущая обновляет состояние программы.

Реальный вклад функциональных языков в параллелизм состоит в повсеместном использовании на высоком уровне особого стиля программирования, при котором операции, такие как map или map-reduce, применяются одновременно ко всем элементам составных структур данных. Такие высокоуровневые операции – хороший источник параллелизма. Этот стиль программирования, к счастью, присущ не только чистым функциональным языкам, но также может быть использован в императивных программах.
К примеру, Google Fellows Jeffrey Dean и Sanjay Ghemawat рассказывают как Google использует Map-Reduce для выполнения масштабируемых, высокопроизводительных вычислений (3). Императивные языки могут благоразумно добавлять функциональные расширения и получать от этого существенные выгоды. Это важно, потому что индустрия не может просто начать все с начала. Постепенное добавление поддержки параллельных возможностей очень важно для сохранения огромных инвестиций в существующие приложения, пока существует задел опыта и навыков разработчиков в императивных языках.

Лучшие абстракции

Большинство сегодняшних языков предлагают явную поддержку программирования на уровне потоков и блокировок (threads and locks). Это низкоуровневые абстракции. Поскольку эти конструкции представляют скудный базис для построения абстракций более высокого уровня, это постоянно поощряет разработчиков на использование многопоточного программирования с присущими ему проблемами произвольных блокировок и реентерабельности.

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

Два базовых примера таких высокоуровневых абстракций это асинхронные вызовы (asynchronous calls) и фьючерсы (futures). Асинхронный вызов, это функция или метод, вызов которой происходит в неблокирующем режиме. Вызывающий код продолжает исполняться, а соответствующее сообщение посылается задаче или вилке (task or fork) на выполнение операции независимо. Фьючерс, это механизм для получения результата от асинхронного вызова, это заместитель (placeholder) для значения, которое еще не материализовалось.

Другой пример абстракции высокого уровня, это активный объект, который концептуально исполняется в своем собственном потоке, таким образом, что создав 1000 таких объектов мы создаем 1000 потенциальных потоков исполнения. Активный объект ведет себя как монитор, в котором только один метод может исполняться в данное время, но он не требует для этого использования традиционных блокировок. Вместо этого, внешние вызовы методов активного объекта представляют собой асинхронные сообщения, которые маршализуются и ставятся в очередь на обработку объектом. Активные объекты могут иметь самый разнообразный дизайн, от специализированных языковых конструкций до COM объектов в модели single-threaded apartments, вызываемого из традиционного кода на C.

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

среда, июля 25, 2007

Программное обеспечение и параллельная революция (Часть 3)

Херб Саттер (Herb Sutter) и Джэймс Лэрус (James Larus), Microsoft.

Часть 3. Блокировки

Проблема разделяемого состояния, и почему блокировки не являются решением

Другим сложным аспектом неструктурированного параллелизма являются разделяемые данные. Клиентское приложение обычно манипулирует разделяемой памятью, организованной как связанный непредсказуемым образом граф объектов.
Когда две задачи пытаются получить доступ к одному объекту, и одна из них желает изменить его состояние, мы попадаем в ситуацию гонки данных (data race). Гонки это плохо, потому что параллельные задачи могут записать и прочитать неконсистентные или поврежденные данные.

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

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

Проблема композирования вызвана главным образом возможностью наступления клинча (deadlock). Простейший вариант клинча наступает, когда две задачи пытаются получить две блокировки во встречном порядке: задача Т1 получает блокировку L1, задача T2 получает блокировку L2, затем, задача T1 пытается получить блокировку L2, а задача T2 пытается получить блокировку L1. Обе задачи блокируются. Поскольку такая ситуация может произойти в любой момент времени, и две блокировки могут быть вызваны во встречном порядке, вы не можете контролировать когда удерживаемая в вашем коде блокировка может стать причиной клинча.

Именно это постоянно происходит в расширяемых средах (frameworks), когда они вызывают виртуальные функции удерживая в это время блокировки (Прим. переводчика. Имеется ввиду, что в переопределенной в наследнике виртуальной функции может быть попытка взять ту же самую блокировку и это станет причиной клинча). Все лучшие на сегодня коммерческие среды не лишены этого недостатка, включая .Net Framework и стандартные библиотеки Java. Мы до сих пор не выбросили их на свалку лишь потому, что разработчики еще не начали писать на них большого количества параллельных приложений. Для решения проблемы клинчей предлагалось множество сложных моделей, например backoff-and-retry протокол, но все они требуют строгой дисциплины при кодировании, а некоторые порождают собственные проблемы (напр., livelocks (активные тупики)).

Техника, позволяющая избегать клинчей, состоит в организации получения блокировок в безопасном порядке не зависимо от того, где они получаются. Например, выравнивание блокировок (lock leveling) и иерархии блокировок (lock hierarchies) препятствуют неправильному порядку вызова блокировок, путем требования того, чтобы все блокировки одного уровня вызывались одновременно в заранее определенном порядке, и в течении времени пока удерживаются блокировки одного уровня вы можете получить дополнительную блокировку только на более высоком уровне. Такая техника хорошо работает внутри одного модуля, или пакета, созданного для внутреннего использования, когда вы можете контролировать весь исходный код. Но ее использование сильно ограничено для расширяемых сред, систем с подключаемыми модулями, и в других подобных случаях, когда система компонуется из кода различных компонент.

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

Попытки локализовать синхронизацию никогда не удаются. Рассмотрим такое популярное решение как Java synchronized methods. Любой метод класса может получить блокировку на экземпляр объекта так, что никакие два потока не смогут изменять состояние объекта одновременно. До тех пор пока состояние объекта изменяется только посредством вызова его методов, и программист не забывает добавлять к методам объявление synchronized, этот подход работает.

Но существует по крайней мере три проблемы, связанные с синхронизированными методами. Первое, они не подходят для типов, методы которых вызывают виртуальные методы других объектов (напр., Vector в Java, или SyncHashTable в .Net), потому что вызов стороннего кода, в то время как удерживается блокировка, открывает возможность появления клинча. Второе, синхронизированные методы могут выполнять слишком много блокировок, запрашивая их для всех экземпляров объектов, несмотря на то, что большинство из них никогда не разделяют своего состояния между несколькими потоками. Третье, синхронизированные методы могут выполнять слишком короткие блокировки, которые не обеспечивают атомарного изменения состояния при вызове нескольких методов объекта или нескольких объектов. В качестве простого примера, иллюстрирующего последнее утверждение, рассмотрим банковский перевод:

account1.Credit(amount); account2.Debit(amount);

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

Альтернативы блокировкам

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

Вторая альтернатива, это транзакционная память, которая привнесла центральную идею о транзакциях из мира баз данных в мир языков программирования. Программисты пишут свои программы в виде серии явно объявленных атомарных блоков, которые исполняются неделимо. Таким образом, параллельно исполняемые операции видят разделяемые данные либо строго до, либо после своего исполнения. Многие рассматривают транзакционную память как многообещающее направление, однако на сегодняшний день она все еще является объектом активных исследований.

вторник, июля 24, 2007

Программное обеспечение и параллельная революция (Часть 2)

Херб Саттер (Herb Sutter) и Джэймс Лэрус (James Larus), Microsoft.


Часть 2. Параллелизм

Программные модели

Сегодня вы можете реализовать параллелизм различными способами, каждый из которых подходит для решения своего класса задач. Во многих случаях крайне трудно, без тщательного анализа и проектирования, заранее определить какая модель подходит для решения конкретной задачи, и еще сложнее совместить несколько моделей, если ваше приложение не вписывается в рамки какой то одной парадигмы.
Все эти модели параллельного программирования различаются в основном по двум направлениям: по степени детализации параллельных операций и по степени связности задач. Эти направления задают своеобразные оси пространства, на котором каждой модели соответствует своя точка. Давайте рассмотрим их.
Операции, исполняемые параллельно могут варьироваться от отдельных инструкций, таких как сложение или умножение, до сложных программ, которые могут исполняться часы или дни. Очевидно, что для мелких операций накладные расходы параллельной инфраструктуры очень важны, например, параллельное исполнение инструкций обычно требует наличия аппаратной поддержки. Многоядерные процессоры снижают расходы на взаимодействие и синхронизацию по сравнению с обычными многопроцессорными системами, которые также способны снизить бремя накладных расходов для малых порций кода. В целом, более мелкие параллельные задачи требуют большего внимания в вопросах разделения задач, а также их синхронизации и коммуникации.
Другое измерение это степень связности в области синхронизации и взаимодействия между операциями. Идеал - нулевая связность: операции выполняются полностью независимо и производят независимые результаты. В этом случае операции могут выполняться в произвольном порядке, не требуют синхронизации и взаимодействия, легко программируются без угрозы возникновения «гонок данных» (data races). Такое положение дел встречается крайне редко, и большинство параллельных программ разделяют свои данные между операциями. Сложность обеспечения правильной и эффективной работы возрастает, по мере того, как операции становятся более разнообразными. В простейшем случае исполняется один и тот же код в каждой операции. Этот тип параллелизма часто называют регулярным, и он может быть достаточно просто проанализирован. Более сложен нерегулярный параллелизм, при котором операции разнообразны, а шаблоны реализации гораздо более сложны для понимания.

Независимый параллелизм.

Пожалуй, наиболее простая модель параллельных вычислений, это независимый параллелизм (в англоязычной среде для нее существует термин “embarrassingly parallel tasks”), в которой одна или несколько операций независимо применяются к каждому элементу в коллекции данных.
Параллельная обработка гранулярных данных основана на независимости операций выполняемых параллельно. Они не должны разделять исходные данные и результаты и могут выполняться без общей координации. Например:
double A[100][100];
…A = A * 2;
умножение каждого элемента массива размерностью 100 х 100 на 2 и сохранение результатов в этом же массиве. Каждое из 10000 умножений может быть выполнено независимо друг от друга. Это вероятно будет даже «слишком параллельно» для большинства компьютеров, более практичный подход состоит в разделении матрицы на блоки размерностью n x m и выполнение операций над этими блоками параллельно.
На другом конце рассматриваемой нами оси детализации, располагаются приложения, такие как поисковики, которые разделяют одну большую базу данных только для чтения, поэтому параллельная обработка поисковых запросов не требует взаимной координации. Другой класс приложений с независимым параллелизмом, это приложения имитационного моделирования, которые требуют многократных прогонов на огромных массивах вариантов входных параметров.

Регулярный параллелизм

Следующим шагом после независимого параллелизма будет выполнение повторяющихся операций над коллекцией данных при том, что вычисления взаимно зависимы. Операция над определенной порцией данных зависит от другой операции, если существует определенная связь или необходимость синхронизации между двумя операциями.
Например, рассмотрим трафаретное вычисление (stencil computation) которое замещает значение каждого элемента двумерного массива средним арифметическим значений четырех соседних элементов:
A[i, j] = (A[i-1, j] + A[i, j-1] + A[i+1, j] + A[i, j+1]) / 4;
Такой алгоритм требует тщательной координации для обеспечения того, что значение массива будет прочитано его соседями, до того как оно будет замещено средним арифметическим. Если нет ограничений по размеру, то значения могут быть помещены в новый массив. В целом, другие, более структурированные стратегии вычислений, такие как диагональное рассечение массива (traversing the array in a diagonal wavefront) дадут такой же результат, но с лучшими показателями кэширования и меньшим потреблением памяти.
Программы с регулярным параллелизмом могут потребовать тщательной синхронизации и оркестрации для получения корректных результатов, однако в отличие от обобщенного параллелизма, код таких операций может быть проанализирован на предмет того, как его можно исполнить параллельно и какие данные он будет разделять. Это преимущество часто оказывается призрачным, потому что программный анализ не является точной дисциплиной, и компиляторы не способны разобрать и реструктуризировать достаточно сложные программы.
На другом конце оси детализации расположены вычисления на web сайтах, которые являются независимыми за исключением обращений к общей базе данных. Вычисления выполняются параллельно без существенной координации, которая сосредоточена в основном в рамках SQL транзакций. Таким образом, обеспечивается согласованный конкурентный доступ к разделяемым данным в базе.

Неструктурированный параллелизм

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

понедельник, июля 23, 2007

Программное обеспечение и параллельная революция

Вы в курсе, что сегодня мы все стоим на пороге революции в разработке программного обеспечения? Революции похлеще той, что развернулась в начале 90-х годов, когда объектно-ориентированные языки вышли из военных и научных лабораторий и в виде первых интегрированных сред разработки пошли в широкие массы программистов. Если нет, то вот вам перевод статьи Херба Саттера (Herb Sutter) и Джэймса Лэруса (James Larus) «Software and the Concurrency Revolution». Довольно популярная на западе (и довольно старая) статья до сих пор не была переведена на русский язык.
Поскольку размер статьи достаточно большой, я буду публиковать ее перевод частями, в соответствии с тем, как опубликован оригинал в ACM Queue

Программное обеспечение и параллельная революция



Херб Саттер (Herb Sutter) и Джэймс Лэрус (James Larus), Microsoft.


Использование всей мощи многоядерных процессоров требует новых инструментов и нового мышления от программной индустрии.

Часть 1. Последствия

Параллелизм уже давно декларируется как «последняя крутая фича» и «путь в светлое будущее», однако за последние 30 лет, основные направления в разработке программного обеспечения умудрялись его игнорировать. Однако наше параллельное будущее наконец то наступает: новые компьютеры будут действительно параллельными и это потребует значительных изменений в способах разработки ПО.
Во вводной статье этого номера (“The Future of Microprocessors” by Kunle Olukotun and Lance Hammond) рассматриваются аппаратные императивы которые определяют переход от монолитных к многоядерным процессорам, также известным как CMPs (chip multiprocessors). (Для анализа см. “The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software.”)(1)
В этой статье мы сосредоточимся на влиянии, оказываемом параллелизмом на программное обеспечение, и на тех последствиях, которые он влечет для программ и программистов.
Изменения в аппаратном обеспечении, которые описывают в своей статье Kunle Olukotun and Lance Hammond являются фундаментальными, и влекут изменения в компьютинге в целом. За последние три десятилетия прогресс в области полупроводников и процессорных архитектур создавал устойчивый рост скорости, с которой компьютеры исполняют существующие последовательные программы. Архитектурные изменения в многоядерных процессорах способны принести реальную отдачу только для параллельных приложений, и поэтому от них мало проку при использовании большинства из существующего ПО. В обозримом будущем десктопные приложения не будут работать более быстро, чем сегодня. А фактически, они могут оказаться и более медленными на новейших чипах, поскольку отдельные процессорные ядра становятся проще и работают на более низких частотах в целях сокращения общего энергопотребления многоядерных процессоров.
Это подводит нас к поворотной точке в области разработки ПО. Компьютеры продолжат становиться все более производительными, однако программы не смогут более повышать свою производительность, просто оседлав волну роста аппаратных возможностей, если только они не станут параллельными.
Помимо многоядерных процессоров есть и другие причины для использования параллельных вычислений, такие как асинхронное выполнение вычислений для улучшения отклика. Например, современные приложения могут выносить часть работы за пределы GUI потока, чтобы дать ему возможность обрисовывать экран, пока производятся вычисления в фоновом режиме.
Но параллелизм сложен. Современные языки и инструменты неадекватны задаче превращения приложений в параллельные программы, и как следствие, сложно отыскать параллелизм в современных массовых приложениях. Но хуже всего то, что параллелизм требует соответственно мыслящих программистов, которых также очень трудно найти.
Тем не менее, будущее за многоядерными машинами и мы должны понять, как их программировать. Оставшаяся часть статьи покажет, почему это сложно, и какие существуют пути решения.

Последствия: новая эра в области программного обеспечения

Сегодня параллельные языки и инструменты находятся на том же уровне, что и обычные языки в начале эры структурного программирования. Семафоры и сопрограммы - это ассемблер параллелизма, блокировки и потоки – это несколько более высокий, структурированный уровень параллельных вычислений. То, что нам нужно – это абстракции высокого уровня, помогающие создавать параллельные программы, подобно тому, как ООП помогает создавать сложные, составные компонентные приложения.
По ряду причин, параллельная революция может оказать на разработку ПО большее влияние, чем ООП революция. Во-первых, параллелизм будет неотъемлемой частью высокой производительности. Языки, не поддерживающие ООП, такие как C, продолжали использоваться для многих программ. Если же параллелизм станет ведущей чертой высокопроизводительного железа, ценность коммерческих и системных языков программирования будет напрямую зависеть от степени поддержки в них параллельного программирования. Существующие языки, такие как C, получат параллельные возможности в виде простых моделей, таких как PThreads. Языки которые не поддерживают параллельное программирование постепенно отомрут и будут использоваться только для поддержки старого оборудования.
Вторая причина, по которой параллелизм более серьезно повлияет на разработку, чем ООП, состоит в том, что параллельное программирование явно сложнее обычного. Например, контекстный анализ последовательных программ является фундаментальной техникой, в соответствии с которой, мы принимаем во внимание контекст вызова при анализе программы. Параллельное программирование также требует анализа синхронизации, однако одновременное применение обеих техник анализа представляется практически неразрешимой задачей (2).
И, наконец, люди быстро устают от параллелизма, они находят его слишком сложным для того чтобы использовать его на равных с последовательным программированием. Ведь даже очень внимательный человек легко может упустить возможные взаимные влияния при одновременном исполнении нескольких операций.

Различия между клиентскими и серверными приложениями.

Параллелизм, это проблема, которая стоит перед клиентскими приложениями. Для
большинства серверных приложений, параллелизм, в общем-то «решенная проблема», потому что обычно мы создаем решения которые довольно хорошо работают в параллельном режиме, однако создание их и обеспечение масштабируемости по-прежнему стоит громадных усилий. Эти приложения обычно изобилуют параллелизмом, потому что они одновременно обрабатывают множество независимых потоков запросов. Например, web сервер или web сайт независимо исполняют тысячи копий одного и того же кода, оперирующего преимущественно независимыми данными.
Кроме того, эти потоки исполняющегося кода хорошо изолированы и разделяют состояние посредством абстрактных хранилищ данных, таких как базы данных, которые поддерживают доступ к структурированным данным с высокой степенью параллелизма. Со стороны нам кажется, что этот код работает с разделяемыми данными «легко и просто», но это всего лишь иллюзия жизни в чистенькой однопоточной вселенной.
Мир клиентских приложений далеко не так структурирован и регулярен. Типичное клиентское приложение выполняет сравнительно небольшое количество вычислений для одного конкретного пользователя, поэтому параллелизм используется в основном для разбиения этих вычислений на более мелкие части. Эти части, скажем к примеру, пользовательский интерфейс (UI) и программные вычисления, взаимодействуют и разделяют между собой общие данные мириадами различных способов. Основные причины, которые делают этот тип приложений сложными для параллельного исполнения это: неоднородный код, высокая гранулярность, сложные взаимодействия и структуры данных, основанные на указателях.

Ищите работу - читайте между строк.

Вот, прочитал у Михаила Елашкина, очередную порцию юмора, о том как надо расшифровывать HR термины в описаниях вакансий:
Конкурентоспособная зарплата: наша компания остается на плаву только потому что мы платим меньше чем конкуренты.
Иногда возможна работа во внеурочное время: иногда каждую ночь, иногда каждые выходные.
Самостоятельность: руководство не будет отвечать на ваши вопросы
Домашняя атмосфера: мы платим так мало, что не рассчитываем что этого хватит нашим сотрудникам на хорошие костюмы.
Требуются навыки PR: мы в полной заднице, и вам понадобиться выступать на ТВ, чтобы вытащить нас оттуда.
Ваши обязанности могут варьироваться: любой человек в офисе может вам приказывать
Ищется кандидат с очень большим опытом и знаниями: вам нужно будет заместить троих наших сотрудников, которые недавно уволились
Умение решать проблемы обязательно: у нас в компании полный бардак
Хорошие коммуникационные навыки: запомни, когда говорит твой босс, ты должен должен заткнуться и внимать тому, что ты должен делать!
Умение работать при больших нагрузках: будешь ныть, уволим нафиг!
Гибкое расписание: работаешь 55 часов в неделю, платим за 37.

В каждой шутке есть доля шутки. Реальность такова, что эта шутка очень близка к истине.
При поиске работы, никогда не предавайте большого значения корпоративной риторике на интервью с HR и будущим вашем менеджером. Наоборот, обилие такой риторики должно вас насторожить, высока вероятность, что в реальности все окажется с точностью до наоборот. Известная доля скептицизма убережет вас от будущих разочарований. Лучше попросите показать свое рабочее место. Посмотрите где, как и на чем сидят сотрудники. С какими выражением лица они провожают взглядом вашего будущего начальника, и как он общается с сотрудниками и с вами. Эти маленькие детали могут сказать о будущей работе много такого, чего никогда не расскажут вам на интервью.

четверг, июля 19, 2007

Калькулятор Google на русском языке

Google порадовал выпуском калькулятора на русском языке. Если кто-то думает что арифметика не зависит от языка, Google доказывает что это не так. Русский калькулятор Google не только знает что такое вершок и аршин, но и знает сколько будет трижды шесть. А вот посчитать сколько будет пятью пять он уже не может.
Интересно, кому может понадобиться возможность узнать как пишется 12345 прописью? Кстати, "12345 in words" тоже будет "двенадцать тысяч...", а вовсе не "twelve thouthand..." :)

понедельник, июля 09, 2007

Русская документация по SQL 2005

Я, конечно, понимаю, что это вовсе не новость для огромного числа IT специалистов. Но вот я только на днях узнал о существовании Документации по SQL Server 2005 на русском языке. Может это из-за того, что являясь разработчиком, я привык искать информацию в MSDN, а эта документация выложена в TechNet.

пятница, июля 06, 2007

Сертификат для SSL шифрования трафика в SQL сервере

Хорошая статья в блоге Yan Liberman о том, как получить, создать и настроить X.509 сертификат для SSL шифрования на SQL сервере.
Помнится, пришлось возться с этой проблеммой - потратил целый день. Жаль, не было под рукой такой статьи.

Сертификат для SSL шифрования трафика в SQL сервере

четверг, июля 05, 2007

Критопаглротимы мгоза

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


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

вторник, июля 03, 2007

"Их нравы" - карьера

В советские времена, под рубрикой "их нравы" публиковались статьи о "загнивающем капитализме". С тех пор многое изменилось, и сейчас мы выступаем в роли усердных (или не очень) учеников у тех самых "загнивающих".
Перевод очень интересной статьи Элизабет М. Ленгвел в блоге Perevedem.ru "Десять карьерных тенденций" наглядно демонстрирует, что учиться нам еще долго.
Рекомендую.

Барьер

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


static void Main(string[] args)
{
// число параллельных потоков
int threadCount = 5;
//запускаем потоки
for (int i = 0; i < threadCount; ++i)
{
Thread t = new Thread(new ThreadStart(ThreadRun));
t.Name = "Thread " + i.ToString();
t.Start();
Console.WriteLine("{0} started", t.Name);
}
Console.WriteLine("All off {0} threads have been finished", threadCount);
Console.WriteLine("End of Main()");
Console.ReadLine();
}

// код для выполнения в потоке
static void ThreadRun()
{
for (int i = 0; i < 10; ++i)
Thread.Sleep(1); // do something
Console.WriteLine("{0} has finished.", Thread.CurrentThread.Name);
}


Выдаст нам, что-то в этом роде:


Thread 0 started
Thread 0 has finished.
Thread 1 started
Thread 2 started
Thread 3 started
Thread 4 started
All of 5 threads have been finished
End of Main()
Thread 1 has finished.
Thread 2 has finished.
Thread 3 has finished.
Thread 4 has finished.


Естественно, нам хотелось, чтоб сообщение “All of 5 threads have been finished” завершало листинг. Для этого нам надо дождаться окончания выполнения всех 5 запущенных потоков. Это и есть задача по синхронизации параллельного исполнения.

Нам надо приостановить основной поток, в котором исполняется метод Main(), дождаться завершения ThreadRun() во всех пяти потоках и затем продолжить исполнение в Main(). В .Net Framework для этих целей служат синхронизационные примитивы, унаследованные от System.Threading.WaitHandle: AutoResetEvent, ManualResetEvent, Mutex. Все они представляют собой managed обертки над различными объектами синхронизации операционной системы. Стоящую перед нами задачу можно решить при помощи ManualResetEvent и статического поля для подсчета числа завершившихся потоков, но лучше всего упрятать детали в специальный класс – Barrier


public class Barrier
{
ManualResetEvent _event;
int _count;
volatile int _current;

/// <summary>
/// Конструктор
/// </summary>
/// <param name="count">"высота" барьера</param>
public Barrier(int count)
{
_event = new ManualResetEvent(false);
_count = count;
_current = 0;
}

/// <summary>
/// Синхронизирующий метод ожидания барьера
/// </summary>
public void Await()
{
if (_current == _count || Interlocked.Increment(ref _current) == _count)
{
_event.Set();
Interlocked.Decrement(ref _current);
}
else
{
_event.WaitOne();
if (Interlocked.Decrement(ref _current) == 0) _event.Reset();
}
}
}


Barrier очень простой класс. Его предназначение, приостанавливать исполняющиеся потоки до тех пор, пока число ожидающих не превысит определенного порога. Величина этого порога (высота барьера) задается в конструкторе. А постановку потока в ожидание осуществляет вызов метода Await().
Использовать его тоже очень просто. В код примера добавляем всего четыре строки (выделены жирным):


static Barrier barrier;
static void Main(string[] args)
{
// число параллельных потоков
int threadCount = 5;
barrier = new Barrier(threadCount+1);
//запускаем потоки
for (int i = 0; i < threadCount; ++i)
{
Thread t = new Thread(new ThreadStart(ThreadRun));
t.Name = "Thread " + i.ToString();
t.Start();
Console.WriteLine("{0} started", t.Name);
}
barrier.Await();// ожидаем преодоления барьера
Console.WriteLine("All of {0} threads have been finished", threadCount);
Console.WriteLine("End of Main()");
Console.ReadLine();
}
// код для выполнения в потоке
static void ThreadRun()
{
for (int i = 0; i < 10; ++i) Thread.Sleep(1); // do something
Console.WriteLine("{0} has finished.", Thread.CurrentThread.Name);
barrier.Await(); // ожидаем преодоления барьера
}


Во-первых, нам понадобится экземпляр Barrier. В конструкторе мы инициализируем его числом threadCount+1 (число запускаемых потоков + 1 главный поток). Далее расставляем вызовы barrier.Await() в тех точках кода, где необходима синхронизация: в конце потокового метода ThreadRun() и в главном методе Main() после запуска всех потоков. Поскольку в конструкторе мы задали «высоту» барьера в 6, то первые пять потоков достигшие вызова метода Await() будут приостановлены, а шестой вызов этого метода разблокирует все ожидающие потоки. Результат налицо:


Thread 0 started
Thread 0 has finished.
Thread 1 started
Thread 2 started
Thread 3 started
Thread 4 started
Thread 1 has finished.
Thread 2 has finished.
Thread 3 has finished.
Thread 4 has finished.
All of 5 threads have been finished
End of Main()


Внутри метода Barrier.Await() увеличивается значение внутреннего счетчика _current и проверяется, что оно не превысило порога _count. В этом случае поток попадает на блокировку _event.WaitOne(). Иначе, блокировка ManualResetEvent сбрасывается и ранее заблокированные потоки освобождаются. Однако код класса Barrier.Await() выглядит, на первый взгляд, довольно странно. Вместо простой проверки if (_current < _count) мы используем странную конструкцию if(_current == _count || Interlocked.Increment(ref _current) == _count). Это сделано для того, чтобы избежать неприятной ситуации, известной под именем “race conditions”. Дело в том, что от момента начала проверки условия до выполнения обусловленного действия (в нашем случае это _event.Set() или _event.WaitOne()), проходит определенное время. В течении этого времени, другой поток может начать исполнять этот же код, и принять не верное решение в операторе проверки (например, оба потока отправятся выполнять _event.WaitOne() и мы получим классический deadlock). Именно поэтому счетчик _current объявлен volatile, а все изменения его значения выполняются при помощи класса Interlocked, который предоставляет нам набор атомарных операций для работы с данными, разделяемыми между потоками. Дополнительная проверка _current == _count введена тоже во избежание “race conditions”.

Почему в методе Barrier.Await не используется обычная блокировка lock(object){….}? Ответ очевиден. Наложение такой блокировки на весь код метода Await() неизбежно приводит в deadlock, а использование lock() только на этапе проверки условия никак не спасает нас от “race conditions”.

Ну и напоследок ложка дегтя. Дело в том, что приведенный в примере класс Barrier не является реентерабельным. Т.е. его экземпляр можно использовать только один раз, несмотря на то, что в Await() есть код, который сбрасывает значение внутреннего счетчика до нуля после преодоления барьера. Для того чтобы сделать наш Barrier реентерабельным и избежать “race conditions”, нам пришлось бы использовать два внутренних ManualResetEvent, и это существенно усложнило бы код.