Задание #6. Знакомство с языком Java

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

До сих пор (возможно, вы уже догадались) все наши вычисления фрактала производились в потоке диспетчере событий Swing. Этот поток обрабатывает все события Swing: нажатие кнопок, перерисовку, и так далее. Именно поэтому пользовательский интерфейс зависает, когда вычисляется фрактал; так как вычисления производятся в потоке диспетчере событий, события не могут обрабатываться до тех пор, пока вычисления не будут закончены.

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

Это в действительности весьма частая ситуация возникающая при проектировании пользовательского интерфейса, пользовательский интерфейс должен инициировать длительную операцию, но операция может исполняться в фоновом потоке в то время как пользовательский интерфейс продолжает обслуживать запросы пользователя. Пожалуй, самый распространенный пример это веб браузеры; во время загрузки и отображения страницы, пользователь может отменить операцию, или щелкнуть по ссылке, или сделать что то еще. Для того чтобы упростить этот способ взаимодействия с пользователем, Swing предлагает класс javax.swing.SwingWorker, который существенно упрощает управление исполнением фоновых потоков. SwingWorker это абстрактный класс; Вы должны расширить его свойства, и добавить функции, необходимые для исполнения потока.  Вот наиболее важные методы, которые требуется реализовать:

doInBackground() – этот метод исполняет фоновую операцию. Swing вызывает этот метод в фоновом потоке, а не в потоке диспетчере сообщений.

done() – этот метод вызывается когда завершается исполнение фоновой операции. Он вызывается в контексте потока диспетчера событий, и следовательно из него доступны компоненты пользовательского интерфейса.

В документации класса SwingWorker есть одна неясность. В действительности это класс SwingWorker<T, V>. Тип T это тип значения возвращаемого doInBackground(), когда исполнение задачи заканчивается. Тип V используется, если фоновая задача передает промежуточные результаты во время работы; эти промежуточные результаты выводятся методами publish() и process(). Использование  одного или обоих этих типов не обязательно; если они не используются, можно просто задать Object для этих типов.

Прорисовка в фоновом режиме

На этой неделе вам придется в основном работать с классом FractalExplorer. Надо будет написать новый код, а также модифицировать уже написанный вами код.

Надо создать дочерний класс SwingWorker и дать ему имя FractalWorker. Это класс должен быть внутренним классом  FractalExplorer. Это наиболее простой способ решить задачу, так как из методов класса понадобится доступ к некоторым внутренним членам класса FractalExplorer. Вспомним, что SwingWorker использует технологию обобщенного программирования, поэтому надо при объявлении класса указать его параметры - укажите Object для обоих, потому что нам они не нужны. В конце концов, получится такое объявление класса:

  private class FractalWorker extends SwingWorker<Object, Object>

Класс FractalWorker будет отвечать за вычисление цветов точек одной строки фрактала, и для этого ему потребуется два поля: целая координата y вычисляемой строки, и массив целых чисел (int) для хранения вычисленных RGB значений каждой точки в строке. Конструктор должен через аргумент получать координату y и сохранять ее; в конструкторе не нужно больше ничего делать. (Например, не надо в этом месте выделять память под массив целых чисел, так как этот массив не нужен до тех пор пока строка не будет вычислена.)

Вспомним, что метод doInBackground() вызывается в фоновом потоке, и выполняет длительную во времени операцию.  Все же возьмите код для этого метода из вашей старой функции “прорисовки фрактала”. Конечно, вместо того чтобы рисовать фрактал на экране, в цикле надо сохранять каждое RGB значение в соответствующий элемент массива целых чисел. Вы не можете изменять изображение на экране из этого потока, потому что это нарушает требования Swing!

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

Метод doInBackground() должен вернуть  нечто типа Object, как указано в спецификации SwingWorker<T, V>. Верните просто null!

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

Как и раньше, после завершения прорисовки строки, надо попросить Swing перерисовать часть экрана, которая была изменена. Так как вы изменили только одну строку, было бы лишним просить Swing перерисовать весь экран! Поэтому, воспользуйтесь вариантом JComponent.repaint() который позволяет задать границы области перерисовки.  Заметим что метод имеет небольшую странность – у него есть неиспользуемый параметр типа long в начале списка аргументов, и вы можете просто подставить ему значение 0. В остальном, просто укажите строку, которую надо перерисовать  – позицию (0, y) и размер (displaySize, 1).

Завершив разработку вашего класса фоновой задачи, встройте его в процесс прорисовки фрактала. Вы уже переместили часть своего кода из функции "прорисовки фрактала" в класс SwingWorker, теперь функцию "прорисовки фрактала" надо изменить:

Для каждой строчки фрактала, создайте отдельный объект SwingWorker и затем вызовите его метод execute(). Этот метод создаст фоновый поток и запустит его на исполнение!

...и это все что нужно изменить! Напомним, что SwingWorker вычислит цвета точек строки и затем прорисует строку на экране, так что теперь ваша функция "прорисовки фрактала" стала очень простой.

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

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

Блокировка событий во время перерисовки

Легче всего решить эту проблему обработки событий во время перерисовки изображения можно следя за количеством строк, которые осталось прорисовать, и игнорируя события от пользовательского интерфейса пока все они не будут перерисованы. Однако делать это следует осторожно, избегая возможных ошибок. Сделаем так: добавим поле "количество оставшихся строк" к классу Fractal Explorer, и будем использовать его для определения конца вычислений. Будем считывать и изменять значение этого поля только в потоке диспетчере событий, чтобы избежать параллельного доступа к полю из нескольких потоков. Если мы будем работать с полем только из одного потока, то сможем избежать различных ошибок связанных с параллельным доступом. Вот что нужно сделать:

Создайте функцию void enableUI(boolean val) которая будет блокировать или разрешать работу кнопок и выпадающего списка в вашем пользовательском интерфейсе в зависимости от значения аргумента. Используйте метод setEnabled(boolean) Swing компонента для блокировки/разрешения работы. Необходимо менять состояние кнопки сохраняющей изображение, кнопки сбрасывающей изображение в начальное состояние и выпадающего списка.

Функция "прорисовки фрактала" должна делать еще две дополнительные вещи. Во первых, метод enableUI(false) надо вызвать в самом ее начале, для того чтобы заблокировать все элементы пользовательского интерфейса перед прорисовкой. Во вторых, надо установить значение "количества оставшихся строк" равным общему числу строк изображения. Сделайте это перед тем как запустить ваши фоновые потоки прорисовки, иначе все это будет работать неправильно!

В методе done() фонового потока, уменьшите "количество отавшийся строк" на единицу как завершающий шаг операции. Затем, если количество оставшихся строк становится равным 0, вызовите enableUI(true).

Наконец, измените обработчик событий от мыши так чтобы он сразу возвращал управление, если "количество оставшихся строк" не равно 0. Иначе говоря, мы будет отвечать на события от мыши только после того как все строки будут прорисованы. (Заметим, что аналогичные изменения в обработчиках команд от компонентов пользовательского интерфейса не нужны, так как мы уже заблокировали все эти компоненты с помощью метода enableUI().)

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


Copyright (C) 2015, California Institute of Technology. All rights reserved.