Я использую это упражнение в качестве педагогического инструмента, чтобы помочь мне вникнуть в некоторые концепции программирования Java GUI. То, что я ищу, - это общее понимание, а не детальное решение одной конкретной проблемы. Я ожидаю, что кодирование этого «права» многому научит меня, как подходить к будущим многопоточным проблемам. Если это слишком общее для этого форума, возможно, оно принадлежит программистам?
Я симулирую кард-ридер. Он имеет графический интерфейс, позволяющий загружать карты в загрузочную воронку и нажимать «Пуск» и т. Д., Но его основным «клиентом» является ЦП, работающий в отдельном потоке и запрашивающий карты.
Устройство чтения карт поддерживает один буфер. Если поступает запрос карты и буфер пуст, считыватель карт должен прочитать карту из накопителя (это занимает 1/4 секунды, то есть 1962). После считывания карты в буфер считыватель карт отправляет буфер в ЦПУ и немедленно инициирует еще одну операцию загрузки буфера до следующего запроса.
Если не только буфер пуст, но в бункере нет карт, то мы должны подождать, пока оператор не поместит колоду в бункер и не нажмет Start (который всегда инициирует операцию загрузки буфера).
В моей реализации запросы карт отправляются на устройство чтения карт в виде invokeLater()
Runnables
, находящегося в очереди на EDT. Во время myRunnable.run()
либо будет доступен буфер (в этом случае мы можем отправить его в ЦП и запустить другую операцию загрузки буфера), либо буфер будет пустым. Что если он пустой?
Две возможности: (a) уже есть операция загрузки буфера в полете, или (b) бункер карты пуст (или еще не был запущен). В любом случае недопустимо задерживать EDT. Работа (и ожидание) должна выполняться в фоновом потоке.
Ради простоты я пытался порождать SwingWorker в ответ на каждый запрос карты, независимо от состояния буфера. Псевдокод был:
SwingWorker worker = new SwingWorker<Void, Void>() {
public Void doInBackground() throws Exception {
if (buffer.isEmpty()) {
/*
* fill() takes 1/4 second (simulated by Thread.sleep)
* or possibly minutes if we need to have another
* card deck mounted by operator.
*/
buffer.fill();
}
Card card = buffer.get(); // empties buffer
/*
* Send card to CPU
*/
CPU.sendMessage(card); // <== (A) put card in msg queue
/*
* Possible race window here!!
*/
buffer.fill(); // <== (B) pre-fetch next card
return null;
}
};
worker.execute();
Это привело к некоторым странным временным эффектам - я подозреваю, что из-за гонки buffer.fill()
, которая может произойти следующим образом: если между (A) и (B) процессор получил карту, отправил запрос на другую. и если от его имени был создан другой поток SwingWorker, то одновременно два потока могут пытаться заполнить буфер. [Удаление вызова предварительной выборки в (B) решило это.]
Так что я думаю, что порождение потока SwingWorker для каждого чтения неверно. Буферизация и отправка карт должны быть сериализованы в одном потоке. Этот поток должен попытаться предварительно извлечь буфер, и должен иметь возможность ждать и возобновлять работу, если у нас закончились карты и нам нужно ждать, пока в накопителе будет размещено больше. Я подозреваю, что SwingWorker имеет то, что требуется, чтобы быть долговременным фоновым потоком, чтобы справиться с этим, но я еще не совсем там.
Предполагая, что поток SwingWorker - это путь, как я могу реализовать это, устраняя задержку на EDT, позволяя потоку блокировать в ожидании повторного заполнения бункера и обрабатывая неопределенность, завершается ли заполнение буфера до или после другой карты запрос приходит?
РЕДАКТИРОВАТЬ: я получил ответ от другой поток и будет резюмировать его здесь:
Вместо использования потока SwingWorker было рекомендовано сначала создать ExecutorService
newSingleThreadExecutor()
один раз, и в нем GUI ставить длинные методы в очередь, используя execute(Runnable foo)
, как показано ниже (этот код выполняется в EDT):
private ExecutorService executorService;
::
/*
* In constructor: create the thread
*/
executorService = Executors.newSingleThreadExecutor();
::
/*
* When EDT receives a request for a card it calls readCard(),
* which queues the work out to the *single* thread.
*/
public void readCard() throws Exception {
executorService.execute(new Runnable() {
public void run() {
if (buffer.isEmpty()) {
/*
* fill() takes 1/4 second (simulated by Thread.sleep)
* or possibly minutes if we need to have another
* card deck mounted by operator.
*/
buffer.fill();
}
Card card = buffer.get(); // empties buffer
/*
* Send card to CPU
*/
CPU.sendMessage(card); // <== (A) put card in msg queue
/*
* No race! Next request will run on same thread, after us.
*/
buffer.fill(); // <== (B) pre-fetch next card
return;
}
});
}
Основное различие между этим и SwingWorker заключается в том, что это обеспечивает наличие только одного рабочего потока.