Проектирование с использованием принципа параллелизма
Поскольку Java все чаще принимается в качестве платформы, многие разработчики перешли к многопоточному программированию. Но программирование с использованием потоков налагает на конструкцию некоторые ограничения – и это хорошо. Эти ограничения настолько полезны, что нам хотелось бы пребывать под их благодатным покровом, когда бы мы ни занимались написанием программ. Это поможет нам делать нашу программу несвязанной и бороться с так называемым "программированием в расчете на стечение обстоятельств" (см. ниже одноименный раздел).
При работе с линейной программой легко сделать предположения, которые в конечном итоге приведут к небрежно написанным программам. Но параллелизм заставляет задумываться о происходящем несколько глубже – вы больше не находитесь в безвоздушном пространстве. Поскольку многие события могут теперь происходить "в одно и то же время", вы можете внезапно столкнуться с зависимостями, основанными на факторе времени. Прежде всего необходимо защитить любые глобальные или статические переменные от параллельного доступа. Теперь можно задать самому себе вопрос, зачем нужна глобальная переменная на первом месте. Кроме того, необходимо убедиться в том, что вы предоставляете непротиворечивую информацию о состоянии независимо от порядка вызовов. Например, в какой момент допускается опрашивание состояния вашего объекта? Если ваш объект находится в недопустимом состоянии в период между определенными вызовами, то вы, вероятно, полагаетесь на стечение обстоятельств – никто не вызовет ваш объект в этот момент времени.
Предположим, что есть подсистема работы с окнами, в которой интерфейсные элементы вначале создаются, а затем отображаются на дисплее. Вам не разрешается задавать состояние в элементе, пока он не отобразится. В зависимости от заданных параметров программы вы можете полагаться на то условие, что ни один другой объект не может воспользоваться созданным элементом, пока вы не выведете его на дисплей.
Но в параллельной системе это может и не выполняться. При вызове объекты всегда обязаны находиться в допустимом состоянии, а они могут вызываться в самое неподходящее время. Вы обязаны убедиться, что объект находится в допустимом состоянии в любой момент, когда потенциально он может быть вызван. Зачастую эта проблема возникает с классами, которые определяют отдельные программы конструктора и инициализации (где конструктор не оставляет объект в инициализированном состоянии). Используя инварианты класса, обсуждаемые в разделе "Проектирование по контракту", вы сможете избежать этой ловушки.
Четкие интерфейсы
Размышления о параллелизме и зависимостях, упорядоченных во времени, могут заставить вас проектировать более четкие интерфейсы. Рассмотрим библиотечную подпрограмму на языке С под названием strtok, которая расщепляет строку на лексемы.
Конструкция strtok не является поточно-ориентированной [33], но это не самое плохое, рассмотрим временную зависимость. Первый раз вы обязаны вызвать подпрограмму Strtok с переменной, которую вы хотите проанализировать, а во всех последующих вызовах использовать NULL вместо этой переменной. Если переменная принимает значение, отличное от NULL, программа повторно производит разбор содержимого буфера. Не принимая во внимание потоки, предположим, что вы собираетесь использовать Strtok для одновременного синтаксического анализа двух отдельных строк:
char buf1[BUFSIZ];
char buf2[BUFSIZ];
char *p, *q;
strcpy(bufl, "это тестовая программа");
strcpy(buf2, "которая не будет работать");
р = strtck(buf1," ");
q = strtok(buf2," ");
while (p && q) {
printf("%s %s\n", p, q);
p = strtok(NULL, " ");
q = strtok(NULL, " ");
}
Представленная программа работать не будет: существует неявное состояние, сохраняющееся в strtok между запросами. Вам придется использовать Strtok одновременно только с одним буфером.
Конструкция синтаксического анализатора строк на языке Java будет отличаться от указанной выше. Она должна быть поточно-ориентированной и представлять непротиворечивое состояние.
StringTokenizer st1 = new StringTokenizer("this is a test");
StrJngTokenlzer st2 = new StringTokenizer("this test will work");
while (st1.hasMoreTokens() && st2.hasMoreTokens()) {
System.out.println(st1.nextToken());
System.out.println(st2.nextToken());
}
Программа StringTokenizer обладает более четким и простым в сопровождении интерфейсом. Она не содержит в себе никаких сюрпризов и в будущем не приводит к появлению таинственных дефектов, чего нельзя сказать о программе Strtok.
Подсказка 41: При проектировании всегда есть место параллелизму
Развертывание
Как только вы спроектировали архитектуру с элементом параллельности, задача об управлении многими параллельными службами упрощается: модель становится всеобъемлющей.
Теперь вы можете проявить гибкость относительно способа развертывания приложения: по автономной модели, модели «клиент-сервер» или по n-звенной модели. Создавая архитектуру системы на основе независимых служб, вы также придаете динамизм процессу конфигурирования. Рассчитывая на параллелизм и разделяя операции во времени, вы получаете вес эти варианты, включая автономный вариант развертывания, где вы можете отказаться от параллелизма.
Другой путь (попытка внести параллелизм в непараллельное приложение) представляется намного сложнее. Если мы проектируем с учетом параллелизма, то со временем нам легче обеспечивать расширяемость и производительность, а если этот момент не настает, мы все равно получаем выгоду от более четкого интерфейса.
Так, может быть, пора?
Другие разделы, относящиеся к данной теме:
• Проектирование по контракту
• Программирование в расчете на стечение обстоятельств
Вопросы для обсуждения
• Сколько задач вы выполняете параллельно, готовясь к работе? Можете ли вы выразить это с помощью диаграммы на языке UML? Можете ли вы найти иной, более быстрый способ подготовки к работе, придав своим действиям больший параллелизм?