litceysel.ru
добавить свой файл
  1 ... 4 5 6 7 8

57. Передача сообщений обеспечивает лучшую масштабируемость параллельных систем

Рассел Уиндер (Russel Winder)

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

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

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

В таком случае, можно ли отказаться от общей памяти? Определенно, да. Вместо потоков и общей памяти можно воспользоваться процессами и передачей сообщений. Под процессом здесь понимается защищенное независимое состояние с исполняющимся кодом, а не обязательно процесс операционной системы. Такие языки, как Erlang (а до него occam) показали, что процессы служат очень удачным механизмом для программирования конкурентных и параллельных систем. В таких системах меньше проблем синхронизации, чем в многопоточных системах с общей памятью. Кроме того, существует формальная модель взаимодействующих последовательных процессов (Communicating Sequential Processes – CSP), которую можно применить при разработке таких систем.


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

При этом для разработки систем применяются главным образом такие языки, как C, C++, Java, Python и Groovy, о которых программистам говорят, что они служат для разработки многопоточных систем с общей памятью. Как же быть? Нужно использовать – или создавать, если их не существует, – библиотеки и среды, которые предоставляют модели процессов и пересылку сообщений, полностью исключая применение общей изменяемой памяти.

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

58. Послание в будущее


Линда Райзинг (Linda Rising)

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

Помню один случай с Джо, слушателем моего курса по структурам данных, который пришел показать мне результат своего труда. «Держу пари, вы не догадаетесь, что делает этот код!» – радостно воскликнул он.

– Вы правы, – согласилась я, не слишком вглядываясь в его текст и думая, как донести до него важную мысль. – Я уверена, что вы старательно потрудились над этим примером. Боюсь, правда, что вы упустили нечто важное. Скажите, Джо, у вас есть младший брат?


– Да, конечно! Его зовут Фил, и он слушает ваш вводный курс. Он тоже учится программировать! – гордо объявил Джо.

– Это замечательно, – отвечала я. – Интересно, сможет ли он понять этот код.

– Ни за что, – сказал Джо, – это крепкий орешек.

– Давайте предположим, – продолжила я, – что это реальный рабочий код и что через несколько лет Филу предложат работу по модернизации этого кода. Хорошую ли услугу вы оказали Филу?

Джо мигал, глядя на меня. «Мы знаем, что Фил – толковый парень, верно?» Джо кивнул. «Должен заметить, я тоже не глуп!» – ухмыльнулся он. «Итак, если мне нелегко понять, что вы тут сделали, и вашему очень способному младшему брату тоже придется поломать над этим голову, то что это говорит о написанном вами коде?»

Как мне показалось, Джо стал смотреть на свой код несколько иначе.

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

– Джо, вы можете написать код, который решает эту задачу, но притом прекрасен, как песня? Да, как запавшая в память мелодия. Я думаю, что тот, кто сумел найти такое сложное решение, как предложенное вами сегодня, может также написать что-нибудь красивое. Гм-м... Не начать ли мне выставлять оценки за красоту? Как вы считаете, Джо?

Джо забрал свою работу и посмотрел на меня. Легкая улыбка пробежала по его лицу. «Я все понял, профессор. Пойду и сделаю жизнь Фила более светлой. Спасибо».

59. Упущенные возможности применения полиморфизма

Кирк Пеппердин (Kirk Pepperdine)

Полиморфизм – одна из грандиозных идей, лежащих в основе ООП. Слово заимствовано из греческого языка и означает много (поли) форм (морф). В контексте программирования полиморфизм означает многообразие форм некоторого класса объектов или метода. Но полиморфизм – это не просто альтернативные реализации. При тщательном применении полиморфизм создает миниатюрные локализованные контексты исполнения, которые позволяют обходиться без пространных блоков if-then-else. Находясь в контексте, мы можем прямо делать нужное дело, тогда как будучи вне этого контекста мы вынуждены воссоздать его, чтобы потом делать нужное дело. При аккуратном использовании альтернативных реализаций можно схватить контекст, который поможет обойтись кодом меньшего объема и лучшей читаемости. Лучше всего продемонстрировать это на примере, скажем, следующего кода для искусственно упрощенной корзинки покупок:

public class ShoppingCart {

private ArrayList cart = new ArrayList();

public void add(Item item) { cart.add(item); }

public Item takeNext() { return cart.remove(0); }

public boolean isEmpty() { return cart.isEmpty(); }

}

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

public class Shipping {

public boolean ship(Item item, SurfaceAddress address) { ... }

public boolean ship(Item item, EMailAddress address { ... }

}

Когда клиент рассчитался, нужно доставить покупки:

while (!cart.isEmpty()) {

shipping.ship(cart.takeNext(), ???);

}

Параметр ??? – это не какой-то очередной Элвис-оператор: он спрашивает, как нужно доставить товар – электронной или обычной почтой. Контекста, требующегося для ответа на этот вопрос, больше не существует. Можно было сохранить метод доставки в виде boolean или enum, а затем использовать if-then-else, чтобы заполнить значение недостающего параметра. Другое решение – создать два класса, расширяющих Item. Назовем их DownloadableItem и SurfaceItem. Теперь напишем немного кода. Я сделаю из Item интерфейс, который поддерживает единственный метод ship (доставка). Чтобы доставить содержимое корзинки, вызываем item.ship(shipper). Оба класса DownloadableItem и SurfaceItem реализуют ship:


public class DownloadableItem implements Item {

public boolean ship(Shipping shipper, Customer customer) {

shipper.ship(this, customer.getEmailAddress());

}

}

public class SurfaceItem implements Item {

public boolean ship(Shipping shipper, Customer customer) {

shipper.ship(this, customer.getSurfaceAddress());

}

}

В этом примере мы делегировали ответственность за Shipping каждому Item. Поскольку каждый товар знает, как его следует доставлять, такая организация позволяет справиться с доставкой, не прибегая к if-then-else. Этот код также демонстрирует применение двух паттернов, которые часто хорошо сочетаются между собой: Command и Double Dispatch. Эффективное использование этих паттернов зависит от тщательного применения полиморфизма. В этом случае количество блоков if-then-else сокращается.

Хотя в некоторых ситуациях гораздо практичнее использовать не полиморфизм, а if-then-else, но полиморфный стиль кодирования чаще позволяет получить код меньшего размера, лучше читаемый и более надежный. Количество упущенных возможностей совпадает с числом операторов if-then-else в коде.



<< предыдущая страница   следующая страница >>