![]() |
Модераторы: LSD, AntonSaburov |
![]() ![]() ![]() |
|
duk |
|
|||
![]() Some Object ![]() Профиль Группа: Участник Сообщений: 179 Регистрация: 19.7.2007 Репутация: 4 Всего: 4 |
каким образом volatile выполняет свои функции. Тоесть с synchronized все ясно, пока какой-то блок или функция не отработает остальные потоки в это время ожидают. а как определяется возможность изменения состояния объекта при использовании поля volatile
|
|||
|
||||
Tamerlann |
|
|||
![]() Бывалый ![]() Профиль Группа: Участник Сообщений: 183 Регистрация: 10.11.2002 Где: Минск, Беларусь Репутация: 2 Всего: 2 |
volatile дает знать виртуальной машине знать, что для этой переменной не нужно выполнять некоторые виды оптимизаций. В конечном счете это приводит так же к тому, что в многопоточном приложении при доступе к переменной ее значение всегда будет актуальным. Иными словами, это тоже способ синхронизации значения переменной. НО!
1. Некоторые виртуальные машины просто игнорируют это ключевое слово 2. Реально синхронизируется ведь только значение переменной! Т.е. если переменная не примитивная, то, очевидно, ничто не мешает делать не синхронизированные вызовы метод объекта переменной. Поэтому, на мой взгляд, этим ключевым словом лучше не пользоваться вовсе. Синхронизацию нужно выполнять с помощью synchronized. Это сообщение отредактировал(а) Tamerlann - 5.2.2008, 00:10 --------------------
http://timursdev.blogspot.com/ |
|||
|
||||
duk |
|
|||
![]() Some Object ![]() Профиль Группа: Участник Сообщений: 179 Регистрация: 19.7.2007 Репутация: 4 Всего: 4 |
Tamerlann, но все же если мы используем блокирование то четко видно в каком месте кода поток прерваться не может. А при использовании volatile а не могу понять каким образом присходит. Например мы считали значение, провели какие-то расчеты, в это время поток приостановился значение данного поля изменилось, и для первого потока оно уже не актуальное.
Меня просто интересует принцип работы (для общего развития). Пытался прочитать на англиском но четких пунктов для себя так и не смог выявить. Если объяснишь буду благодарен. |
|||
|
||||
Sardar |
|
|||
![]() Бегун ![]() ![]() ![]() ![]() Профиль Группа: Модератор Сообщений: 6986 Регистрация: 19.4.2002 Где: Нидерланды, Groni ngen Репутация: 4 Всего: 317 |
volatile переменные это не средства синхронизации, есть только гарантия, что в момент чтения ты всегда будешь получать актуальное значение. Это также значит, что если в выражении эта переменная встречается несколько раз, то столько же раз она будет опрошена из памяти и следовательно в любой момент может изменится, посреди выражения. Больше никаких гарантий нет, если устареет твоё значение, значит так тому и быть
![]() Думаю в 70% случаев когда используют volatile она в действительности не нужна, ибо это зло. -------------------- Опыт - сын ошибок трудных © А. С. Пушкин Процесс написания своего велосипеда повышает профессиональный уровень программиста. © Opik Оценить мои качества можно тут. |
|||
|
||||
Tamerlann |
|
|||
![]() Бывалый ![]() Профиль Группа: Участник Сообщений: 183 Регистрация: 10.11.2002 Где: Минск, Беларусь Репутация: 2 Всего: 2 |
В том то и дело, что для любого потока занчение переменной всегда будет актуально. Даже если выполнение одного потока прервалось, и второй поток изменил значение переменной, то когда продолжит выполняться первый поток - значение переменной будет актуально.
Чтобы понять как это достигается, скорее нужно понять почему может произойти обратное. Т.е. почему может произойти "рассинхронизация"? Рассинхронизация происходит потому что каждый поток может иметь свой кэш для хранения текущих значений переменных. Например, если посмотреть с "железной" стороны - текущее значение может храниться в регистре одного из процессоров, который и выполняет текущий поток. volatile должен указывать VM, чтобы она всегда хранила значение переменной в "общей" памяти, а не в кэше. Таким образом, какой бы поток не получал значение переменной - он всегда обращается к "общей" памяти VM и, соотвественно, получает актуальное ее значение. Добавлено через 14 минут и 18 секунд Если я правильно понял вопрос, то вот продолжение: Имеем код:
разумеется, нет никакой гарантии, что какой-либо другой поток не залезет между строками 5 и 7 и не изменит занчение x. Наоборот, если это произойдет, то значение переменной x будет таким, какое оно получилось после выполнения операций в другом потоке. Поэтому: 1. Не надо открывать паблик доступ к таким переменным. 2. Надо синхронизировать код, изменяющий значение таких переменных. Это сообщение отредактировал(а) Tamerlann - 5.2.2008, 01:07 --------------------
http://timursdev.blogspot.com/ |
|||
|
||||
w1nd |
|
||||||||
![]() Вертилятор ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1077 Регистрация: 22.3.2006 Где: Москва Репутация: 20 Всего: 54 |
Откуда свалилось такое компетентное мнение?
Такие виртуальные машины не являются полноценными, только и всего.
Странная рекомендация. Модификатор volatile имеет вполне определённое назначение и не пользоваться им в некоторых ситуациях попросту не удастся. Данный тип переменных является свойственным отнюдь не для java, а для любой многопоточной среды. Другой вопрос - с синхронизацией сценарии его использования действительно имеют мало общего. Добавлено @ 01:50
Далеко не всегда оправданная стратегия. Так как все операции в java атомарны, нет ничего зазорного в том, чтобы просто получать актуальное состояние переменной, тогда как инициатор этого изменения не важен. Это сообщение отредактировал(а) w1nd - 5.2.2008, 01:51 -------------------- ![]() ![]() |
||||||||
|
|||||||||
Sardar |
|
|||
![]() Бегун ![]() ![]() ![]() ![]() Профиль Группа: Модератор Сообщений: 6986 Регистрация: 19.4.2002 Где: Нидерланды, Groni ngen Репутация: 4 Всего: 317 |
Первое слово "думаю", впечатление сложилось перебирая прошлогодние лабы по concurrency.
Volatile к многопоточной среде не имеет никакого отношения. Изначально штука создавалась для считывания регистров различных девайсов, корнями уходит в C. В современном мире даже явные блокировки (семафоры, мониторы etc) уже считаются не удобными/устаревшими, за этим должен следить компилер. Программист должен использовать функции без side-effects и не изменяемые "переменные", на манер того как это делает Erlang. Другими словами "переменная" не должна изменятся, должна создаваться её новая копия и тем более не должны быть переменные, что могут сами внезапно изменится. Иначе появляется весь букет проблем, race-conditions, deadlocks, излишние не явные случайные блокировки, из-за которых совершенно случайно может упасть производительность и.т.п. Венцом конечно будет крайне плохо оптимизируемая работа с памятью. -------------------- Опыт - сын ошибок трудных © А. С. Пушкин Процесс написания своего велосипеда повышает профессиональный уровень программиста. © Opik Оценить мои качества можно тут. |
|||
|
||||
w1nd |
|
|||
![]() Вертилятор ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1077 Регистрация: 22.3.2006 Где: Москва Репутация: 20 Всего: 54 |
Также как к многопоточной и многопроцессорной среде не имеют отношения потоки вообще. Смешно. В современном мире процессоры пока остаются принципиально такими же, какими были и десяток-другой лет назад. Отслеживание всех возможных коллизий компилятором (а на самом деле - банальная безусловная блокировка на каждый чих) в первую очередь приведёт к снижению производительности в многопоточных и многопроцессорных системах на порядки. Volatile-переменная - это такая переменная, буферизация которой исполняющей средой или процессором недопустима. ВСЁ. И отношение к многопроцессорной и многопоточной среде этот модификатор имеет самое непосредственное - потрудитесь вспомнить о том, что упомянутые вами девайсы работали асинхронно с процессором. Это сообщение отредактировал(а) w1nd - 5.2.2008, 02:26 -------------------- ![]() ![]() |
|||
|
||||
Sardar |
|
|||
![]() Бегун ![]() ![]() ![]() ![]() Профиль Группа: Модератор Сообщений: 6986 Регистрация: 19.4.2002 Где: Нидерланды, Groni ngen Репутация: 4 Всего: 317 |
Советую расширить кругозор, желательно пройти курс concurrency в любом (нормальном) универе. Да, критические секции ещё используются (но не на высокоэффективных распределённых системах), но за этим следит компилер, который действительно использует минимум подобных исключений (иногда ошибка синхронизации настолько не существенна, что и критические секции отменяются). Компилер это делает гораздо эффективней, чем человек (чел. ленив по определению), цена этому конечно свобода. Чем больше свободы у человека, тем меньше возможности компилятора для оптимизаций. Именно поэтому строят высокоуровневые языки, где многие вещи решаются однотипно, чуть ли не по сценарию, полностью абстрагируясь от внутреннего устройства/реализации. Пример, тот же Erlang, где не используются прямые блокировки, расшаренная память и т.п.
Volatile переменная зачастую использовалась для busy-waiting'овых циклов ожидания до устройства. Почти всегда необходимое состояние на железном регистре приходило в короткое время, поэтому иногда в таком цикле отключали прерывания (что конечно криво), т.е. "многопоточность" могла "отключиться" либо её вообще не использовали по дизайну. У меня первый диплом в этой области ![]() К организации или управлению многопоточностью volatile переменные не применялись. Вообще в многопоточной системе есть пара основных принципов:
Только так можно построить высокоэффективную (читай быструю) систему, без ошибок, без deadlock'ов и реально масштабируемую (без большого overhead'а из-за многопоточности). Мы говорим о реальных результатах/академической корректности, а не о практике, деньгах и хорошем маркетинге ;-) Всё, наша дискуссия исчерпала себя, продолжение можно найти в PM. Эту тему засорять не стоит. -------------------- Опыт - сын ошибок трудных © А. С. Пушкин Процесс написания своего велосипеда повышает профессиональный уровень программиста. © Opik Оценить мои качества можно тут. |
|||
|
||||
Platon |
|
|||
![]() Эксперт ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1801 Регистрация: 25.4.2006 Репутация: 16 Всего: 40 |
Нет уж! Никаких ПМов, народу (мне) интересно!
|
|||
|
||||
Kangaroo |
|
|||
![]() AA - Aussie Animal ![]() ![]() ![]() ![]() Профиль Группа: Участник Клуба Сообщений: 2042 Регистрация: 7.10.2006 Где: US Репутация: 21 Всего: 104 |
Поддерживаю. Нечего скрывать драгоценные знания от народа ![]() -------------------- Lost.... |
|||
|
||||
w1nd |
|
||||
![]() Вертилятор ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1077 Регистрация: 22.3.2006 Где: Москва Репутация: 20 Всего: 54 |
Встречный совет - спуститься с небес на грешную землю. Компилятор по определению не может управлять подобными подсистемами эффективнее человека, тем более без изменения уровня абстракций. Да, желание избежать влияния человеческого фактора понятно, но принципиально нереализуемо - это доказывает уже многолетняя практика создания ПО во всём мире. Языки, в которых будут реализованы описанные стратегии, станут эффективны лишь тогда, когда производительность железа возрастёт на порядок.
Я не припоминаю, чтобы говорил о управлении или организации многопоточностью с помощью volatile-переменных. Вы уже не приписывайте мне свои фантазии. Я говорил о таком типе переменных, как о непременном атрибуте многопоточных и многопроцессорных сред. Как раз для ситуаций, когда упомянутые вами "основные принципы" исключают или затрудняют возможность реализации задачи. Вообще, смешно читать о гипотетическом многопоточном окружении, где не используются блокировки и разделяемая память. Это всё будет использоваться ВСЕГДА, вопрос только - явно или не явно. Для того, чтобы избежать явного управления некоторыми процессами (дабы исключить грубые ошибки), следует искоренить всякую возможность взаимодействия с этими процессами, кроме конфигурационного управления. Платой за такой подход неизбежно станет потеря эффективности, а уж о том, что все эти песни не про java - и не говорю. Это вас куда-то в сторону занесло, volatile-переменные - это всегда такие данные, которые бесконтрольно могут меняться извне, смысл от контекста применения не меняется. Добавлено @ 13:40 Ну, все сведения по вопросу уже представлены, началась уже банальная фаллометрия ![]() Это сообщение отредактировал(а) w1nd - 5.2.2008, 13:47 -------------------- ![]() ![]() |
||||
|
|||||
Sardar |
|
|||
![]() Бегун ![]() ![]() ![]() ![]() Профиль Группа: Модератор Сообщений: 6986 Регистрация: 19.4.2002 Где: Нидерланды, Groni ngen Репутация: 4 Всего: 317 |
Достаточно голословное утверждение. Можешь провести тесты и убедится, что встроенные в язык решения всегда быстрей, чем аналогичное "в ручную" (извиняюсь, тоже голословное, результаты тестов потерял). Всё это от лени, человек всегда ищет чего попроще. К примеру создание треда в C - чуть ли не подвиг, из-за постоянных locks треды работают жутко не эффективно (не только ожидание критической секции, но и сбросы кеша и т.п.), почти всегда в новых тредах/подпроцессах запускается единый однотипный хендлер (ftp/http сервер, тред обслуживающий запрос). Теперь сравни как работает с тредами Mozart/OZ, треды на столько дешёвые, что любое событие/сообщение отрабатывается в отдельном треде. Мы говорим о реально сотнях/тысячах кратко-живущих тредов, идеальная среда для сетки Cell процессоров. Из-за жёстких ограничений в языке можно гарантировать целостность состояния в run-time, исключаются блокировки, что делает возможным просто "запустить тред", который эффективно отработает как у себя (parallelized) , так и на чужой машине (distributed). Человек в ручную просто не способен уследить за всем этим и глупо даже пытаться, человек должен видеть только общую картину. Да, я верю, что и на асме вы способны написать аналог Eclipse, а полностью распределённые fault tolerant приложения, где каждая мелкая операция выливается в не один десяток строк вызовов низкоуровневого API - для вас лишь детская игра. К сожалению других таких нет, мы простые люди не способны удержать полную структуру программы в голове, учитывая что она ещё зависит от конкретной среды исполнения ![]() Volatile переменная - не стабильна по определению. Она меняется в любой момент, в принципе основное её использование - ожидание хардварного статуса. В нормальных многопоточных средах нет volatile переменных, т.к. они нарушают основной принцип no side-effects. Попробуйте придумать хоть один сценарий, когда volatile переменная могла бы понадобится? Опережу, организовывать конец основного цикла треда через volatile переменную - есть полнейшая глупость. Нет необходимости мгновенно останавливать тред, ценой жуткой потери производительности (помним что современный проц работает в 12+ раз быстрее памяти, это не учитывая разного рода ожидания и bus-lock'и, особенно в многоядерных системах). Убери свойство volatile у переменной и заметишь, что прога по прежнему хорошо работает. А потому что есть гарантия, что переменная рано или поздно обновится, все треды, рано или поздно возьмут актуальное значение. Пусть тред завершится на пару миллисекунд позже, но зато в процессе работы он будет в десяток раз быстрее. Что важно, корректность программы сохраняется. Какие ещё будут примеры? P.S. 2w1nd, если хочешь, будем беседовать без "подколок", народу читать будет приятней. Но что бы было продуктивно, давай примеры и ссылки на свои утверждения. -------------------- Опыт - сын ошибок трудных © А. С. Пушкин Процесс написания своего велосипеда повышает профессиональный уровень программиста. © Opik Оценить мои качества можно тут. |
|||
|
||||
duk |
|
|||
![]() Some Object ![]() Профиль Группа: Участник Сообщений: 179 Регистрация: 19.7.2007 Репутация: 4 Всего: 4 |
Спасибо всем, кто поучаствовал в обсуждении. Вы мне помогли.
Привожу инфу, которую я нашел по этой теме (может кому-то будет полезной): • use - чтение значения переменной из рабочей памяти потока • assign - запись значения переменной в рабочую память потока • read - получение значения переменной из основного хранилища • load - сохранение значения переменной, прочитанного из основного хранилища, в рабочей памяти • store - передача значения переменной из рабочей памяти в основное хранилище для будущего сохранения • write - сохраняет в основном хранилище значение переменной, переданной командой store При объявлении полей объектов и классов может быть указан модификатор volatile. Он устанавливает более строгие правила работы со значениями переменных. Если поток собирается выполнить команду use для volatile переменной, то требуется, чтобы предыдущим действием над этой переменной было обязательно load, и наоборот - операция load может выполняться только перед use. Таким образом, переменная и главное хранилище всегда имеют самое последнее значение этой переменной. Аналогично, если поток собирается выполнить команду store для volatile переменной, то требуется, чтобы предыдущим действием над этой переменной было обязательно assign, и наоборот - операция assign может выполняться только если следующей будет store. Таким образом, переменная и главное хранилище всегда имеют самое последнее значение этой переменной. Наконец, если проводятся операции над несколькими volatile переменными, то передача соответствующих изменений в основное хранилище должно проводится строго в том же порядке. При работе с обычными переменными компилятор имеет больше пространства для маневра. Например, при благоприятных обстоятельствах может оказаться возможным предсказать значение переменной, заранее вычислить и сохранить его, а затем в нужный момент использовать уже готовым. Нужно обратить внимание на два 64-разрядных типа double и long. Поскольку многие платформы поддерживают лишь 32-битную память, величины этих типов рассматриваются как две переменные, и все описанные действия делаются независимо для двух половинок таких значений. Конечно, если производитель виртуальной машины считает возможным, он может обеспечить атомарность операций и над этими типами. Для volatile переменных это является обязательным требованием. |
|||
|
||||
w1nd |
|
|||
![]() Вертилятор ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1077 Регистрация: 22.3.2006 Где: Москва Репутация: 20 Всего: 54 |
Хорошо, вот вам простой сценарий: N потоков в цикле получают значение переменной, производят некоторые вычисления (какие - не важно), записывают результат в ту же переменную; ещё один отправляет каду-то это значение с некоторой периодичностью. Расскажите в двух словах, как компилятор сможет определить необходимость синхронизации доступа к этой переменной и как он сможет определить, какие конкретно участки кода требуется воткнуть в synchronized.
Вот тут мы никогда не договоримся. Я провожу эти тесты уже на протяжении пятнадцати лет ![]() Это не принцип функционирования реальных систем ("no side-effects"), это идеал. Сферический конь в вакууме, если хотите. А что до примера - на ходу придумать не могу, не встречался, но более чем достаточно возможности такой ситуации в принципе. Ситуации, когда необходимо обойтись без блокировок, но нельзя не замечать динамики изменений некоторых данных. Это сообщение отредактировал(а) w1nd - 5.2.2008, 17:41 -------------------- ![]() ![]() |
|||
|
||||
Sardar |
|
||||||
![]() Бегун ![]() ![]() ![]() ![]() Профиль Группа: Модератор Сообщений: 6986 Регистрация: 19.4.2002 Где: Нидерланды, Groni ngen Репутация: 4 Всего: 317 |
Хороший пример когда volatile переменная вообще не нужна, по сути ведь теряем инфу. Это решается посредством пересылки сообщений, желательно с буффером, чем рендеву. Можно конечно по всякому задачу решить, но эффективным (быстро в run-time) решение с volatile переменной не будет, как я уже говорил, из-за не оправданных блокировок (вкйлучая железные) и отсутствию кеша. Поэтому такие сценарии исключаются в системах нацеленных на эффективную многопоточность/распределённость вычислений. Вывод: компилер не будет оптимизировать этот код (как беспоезно оптимизировать асм, кроме как простейшего pipe), он просто вам не даст писать такой (до жути не эффективный) код.
Да, пустой спор. Вот тут бенчмарки. Компилер оптимизирует согласно вложенной технике, его сила в полном "восприятии" кода, человек же способен "видеть" только ограниченный кусок. Да хотя бы шедулер комманд в декларативном языке (к примеру Haskell) так выдаст инструкции, что бы каждое выражение вычислялось не не более одного раза, к тому же лениво (по требованию). Техника доказуемая формально, это всё что нужно от человека. А компилятор способен "разжевать" массу текста и построить самое эффективное (или приближенное) решение, формально доказуемое. Гений действительно может по иному посмотреть и организовать код, но таких единицы (и они привлекаются к разработке компиляторов/языков), остальные же в силу природной лени, либо пишут не эффективный код, либо пользуются решениями высокоуровнего языка. Каждый сценарий в высокоуровневом языке доказан, эффективность решения обоснованна/доказана, поэтому в целом код почти всегда быстрей, чем аналогичный "полностью в ручную", к примеру на C. Вам стоит заинтересоваться теорией компиляторов, построением формальных языков и среды выполнения. Тогда исчезнут домыслы о искусственном интеллекте и не оправданное недооценивание теоретических основ (программирование - наука, а не только ремесло в форм дизайнере). Впрочем быть может вы правы, нейро-сетки из ИИ области в принципе могут помочь в нахождении общей "методологии"/практики среди группы программистов одного языка, всё таки мощный это инструмент обобщения.
Не знаю с какими системами вы сталкиваетесь каждый день... но хотя бы тот же Erlang - пример, где side-effects по моему полностью исключены.
Можно "отречься" от ранее навязанной вам техники опроса состояния (polling) и просто ждать сообщений, поверь, это гораздо эффективней, особенно если сообщения идут в своих легковесных тредах и работает всё в сетке мелких процев. Наофтопили ![]() -------------------- Опыт - сын ошибок трудных © А. С. Пушкин Процесс написания своего велосипеда повышает профессиональный уровень программиста. © Opik Оценить мои качества можно тут. |
||||||
|
|||||||
w1nd |
|
|||
![]() Вертилятор ![]() ![]() ![]() Профиль Группа: Завсегдатай Сообщений: 1077 Регистрация: 22.3.2006 Где: Москва Репутация: 20 Всего: 54 |
Что-то подсказывает мне, что вы пытаетесь уйти от темы (и про volatile в вопросе ничего не было). Правильный ответ - компилятор не в состоянии определить необходимость синхронизации доступа в подобном коде. Выход один - синхронизировать любые обращения (причём на уровне методов, потому как синхронизация только для копирования может нарушить логику), что приведёт к ужасающим в плане производительности результатам. Но ещё один гвоздик: синхронизация, возможно, не нужна вовсе ![]() Как же любят некоторые бездельники от науки раздувать целые теории из фактов, не стоящих и выеденного яйца ![]() Слушатели - не лучшее решение, чем больше их будет, тем больше времени такая система будет тратить впустую. Я уже не говорю о ситуации, когда у вас попросту нет возможности расширять функционал некоторых подсистем. -------------------- ![]() ![]() |
|||
|
||||
Wissenstein |
|
||||||
![]() Новичок Профиль Группа: Участник Сообщений: 21 Регистрация: 7.8.2007 Где: Харків, Україна Репутация: нет Всего: нет |
могла бы понадобиться Всё уже придумано до нас (© Неизвестный автор).
Предположим, объект aa начинает доступ к этому одиночке, а следом за ним то же делает объект bb:
Если instane не объявлять как volatile, то возможен такой сценарий: Объект aa вызывает проверку (instance == null), одиночка убеждается, что инстанции ещё нет, создаёт в памяти соответствующий объект и начинает его инициализировать, а тем временем в основную память передаётся ссылка на этот объект. Пока идёт инициализация, объект bb вызывает проверку, в ходе которой выясняется, что инстанция существует, и получает ссылку на объект instance. Потом объект сс пытается получить importantValue, но объект instance ещё не инициализирован до конца, и cc получает неправильное значение. Если же instance будет volatile, то ссылка на instance не будет возвращена в основную память, пока не закончится инициализация этого объекта. Таким образом, вызов от объекта bb повлечёт проверку (instance == null) которая вернёт true, и управление будет далее передано в синхронизированный блок, bb дождётся своей очереди, узнает, что объект instance уже создан, и получит ссылку на него; при этом объект будет уже полностью инициализирован, и cc таки гарантированно получит правильное значение importantValue. |
||||||
|
|||||||
![]() ![]() ![]() |
Правила форума "Java" | |
|
Если Вам помогли, и атмосфера форума Вам понравилась, то заходите к нам чаще! С уважением, LSD, AntonSaburov, powerOn, tux, javastic. |
0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей) | |
0 Пользователей: | |
« Предыдущая тема | Java: Общие вопросы | Следующая тема » |
|
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности Powered by Invision Power Board(R) 1.3 © 2003 IPS, Inc. |