Как имитировать буферизованное периферийное устройство с SwingWorker? - PullRequest
7 голосов
/ 12 августа 2011

Я использую это упражнение в качестве педагогического инструмента, чтобы помочь мне вникнуть в некоторые концепции программирования 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 заключается в том, что это обеспечивает наличие только одного рабочего потока.

Ответы [ 3 ]

5 голосов
/ 14 августа 2011

В «ответе», добавленном к первоначальному вопросу, не было ничего:

Я передавал трудоемкую работу (не более, чем Thread.sleep() для педагогических целей) фоновому потоку через Исполнителя с одним потоком. Однако возникла проблема, поскольку фоновый поток «считывал карту» путем опроса () списка, который служил моделью данных для компонента Swing, и повышения большого числа индексов массива AWT за пределами диапазона. После нескольких тщетных попыток синхронизировать доступ к списку как с помощью EDT, так и моего фонового потока, я набрал команды и обернул команды для poll () списка и обновил графический интерфейс в небольшом Runnable (), и использовал invokeAndWait () для запустить их на EDT, пока моя фоновая задача ждала.

Вот мое исправленное решение:

private ExecutorService executorService;
 :
executorService = Executors.newSingleThreadExecutor();
 :
/*
 * When EDT receives a request for a card it calls readCard(),
 * which queues the work 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)
                 */
                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;
        }
    });
}

/*
 * IMPORTANT MODIFICATION HERE - - -
 *
 * buffer fill() method has to remove item from the list that is the
 * model behind a JList - only safe way is to do that on EDT!
 */
private void fill() {
    SwingUtilities.invokeAndWait(new Runnable() {
        /*
         * Running here on the EDT
         */
        public void run() {
            /*
             * Hopper not empty, so we will be able to read a card.
             */
            buffer = readHopper.pollLast();  // read next card from current deck
            fireIntervalRemoved(this, readHopper.size(), readHopper.size()); 
            gui.viewBottomOfHopper(); // scroll read hopper view correctly
        }
    });
    // back to my worker thread, to do 1/4 sec. of heavy number crunching ;)
    // while leaving the GUI responsive 
    Thread.sleep(250);
     :
    etc.
}
5 голосов
/ 13 августа 2011

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

Если вы используете шаблон Модель – Вид – Контроллер , здесь предлагается , ваша модель - это процессор. Хотя это может быть другой класс, я не вижу причин для того, чтобы смоделировать кард-ридер в другом потоке. Вместо этого, пусть модель процессора имеет модель устройства считывания карт, которая выполняет ожидание в потоке java.util.Timer, обновляя модель при срабатывании таймера. Пусть обновленная модель уведомит представление в обычном порядке публикации событий в EDT. Позвольте контроллеру отменить и запланировать модель устройства чтения карт в ответ на жесты просмотра.

4 голосов
/ 13 августа 2011

1) создать графический интерфейс, должен быть пустым или основан на значениях по умолчанию из пакетов Java

2) старт periodic = new AccurateScheduledRunnable() {...};

    periodicMonitor = scheduler.scheduleAtFixedRate(periodic, 0, taskPeriod,
          TimeUnit.MINUTES);

3) объявить монитор для ScheduledFuture<?> periodicMonitor; тогда вы получите, например ... оставшееся время от

periodic = new AccurateScheduledRunnable() {...};

long she = periodicMonitor.getDelay(TimeUnit.SECONDS); 

4) SwingWorker может поддерживать многопоточность с помощью Executor executor = Executors.newCachedThreadPool();, тогда вы сможете этот

5) все, что вы ожидали ...

EDIT

мммм AccurateScheduledRunnable - это пользовательский абстрактный класс

но для моего удовольствия я построил этот, .. дайте ответ, о чем я говорил

enter image description here

import java.awt.*;
import java.awt.event.ActionEvent;
import java.beans.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.swing.*;
import javax.swing.table.*;

public class TableIcon extends JFrame implements Runnable {

    private static final long serialVersionUID = 1L;
    private JTable table;
    private JLabel myLabel = new JLabel("waiting");
    private JLabel lastRunLabel = new JLabel("waiting");
    private int pHeight = 40;
    private boolean runProcess = true;
    private int count = 0;
    private SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
    private ScheduledExecutorService scheduler;
    private AccurateScheduledRunnable periodic;
    private ScheduledFuture<?> periodicMonitor;
    private Executor executor = Executors.newCachedThreadPool();
    private Date dateLast;
    private Date dateNext;
    private Date dateRun;
    private int taskPeriod = 1;
    private int dayCount = 0;
    private int hourCount = 0;
    private int minuteCount = 0;
    private int secondCount = 0;
    private Timer timerRun;
    private int delay = 3000;
    private boolean bolo = false;

    public TableIcon() {
        ImageIcon errorIcon = (ImageIcon) UIManager.getIcon("OptionPane.errorIcon");
        ImageIcon infoIcon = (ImageIcon) UIManager.getIcon("OptionPane.informationIcon");
        ImageIcon warnIcon = (ImageIcon) UIManager.getIcon("OptionPane.warningIcon");
        String[] columnNames = {"Picture", "Description"};
        Object[][] data = {{errorIcon, "About"}, {infoIcon, "Add"}, {warnIcon, "Copy"},};
        DefaultTableModel model = new DefaultTableModel(data, columnNames) {

            private static final long serialVersionUID = 1L;

            @Override
            public Class getColumnClass(int column) {
                return getValueAt(0, column).getClass();
            }
        };
        table = new JTable(model);
        table.setRowHeight(pHeight);
        table.setPreferredScrollableViewportSize(table.getPreferredSize());
        JScrollPane scrollPane = new JScrollPane(table);
        add(scrollPane, BorderLayout.CENTER);
        lastRunLabel.setPreferredSize(new Dimension(200, pHeight));
        lastRunLabel.setHorizontalAlignment(SwingConstants.CENTER);
        add(lastRunLabel, BorderLayout.NORTH);
        myLabel.setPreferredSize(new Dimension(200, pHeight));
        myLabel.setHorizontalAlignment(SwingConstants.CENTER);
        add(myLabel, BorderLayout.SOUTH);
        scheduler = Executors.newSingleThreadScheduledExecutor();
        periodic = new AccurateScheduledRunnable() {

            private final int ALLOWED_TARDINESS = 200;
            private int countRun = 0;
            private int countCalled = 0;

            @Override
            public void run() {
                countCalled++;
                if (this.getExecutionTime() < ALLOWED_TARDINESS) {
                    countRun++;
                    executor.execute(new TableIcon.MyTask("GetCurrTime")); // non on EDT
                }
            }
        };
        periodicMonitor = scheduler.scheduleAtFixedRate(periodic, 0, taskPeriod, TimeUnit.MINUTES);
        periodic.setThreadMonitor(periodicMonitor);
        new Thread(this).start();
        prepareStartShedule();
    }

    private void prepareStartShedule() {
        timerRun = new javax.swing.Timer(delay, startCycle());
        timerRun.setRepeats(true);
        timerRun.start();
    }

    private Action startCycle() {
        return new AbstractAction("Start Shedule") {

            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                executor.execute(new TableIcon.MyTask("StartShedule")); // non on EDT
            }
        };
    }

    private void changeTableValues() {
        Runnable doRun = new Runnable() {

            @Override
            public void run() {
                if (bolo) {
                    bolo = false;
                    table.getModel().setValueAt("*/*/*/**/*/*/*", 0, 1);
                    table.getModel().setValueAt(" k k k k k k k k", 1, 1);
                    table.getModel().setValueAt("@#@#@#@", 2, 1);
                } else {
                    bolo = true;
                    table.getModel().setValueAt("Green Peper", 0, 1);
                    table.getModel().setValueAt("Yellow Apple", 1, 1);
                    table.getModel().setValueAt("Orange Bus", 2, 1);
                }
            }
        };
        SwingUtilities.invokeLater(doRun);
    }

    private void distAppInfo() {
        Runnable doRun = new Runnable() {

            @Override
            public void run() {
                dateNext = new java.util.Date();
                dateLast = new java.util.Date();
                long tme = dateNext.getTime();
                tme += (taskPeriod * 60) * 1000;
                dateNext.setTime(tme);
                lastRunLabel.setText("Last : " + sdf.format(dateLast) + " / Next : " + sdf.format(dateNext));
            }
        };
        SwingUtilities.invokeLater(doRun);
    }

    private void changeLabelColor() {
        Runnable doRun = new Runnable() {

            @Override
            public void run() {
                Color clr = lastRunLabel.getForeground();
                if (clr == Color.red) {
                    lastRunLabel.setForeground(Color.blue);
                } else {
                    lastRunLabel.setForeground(Color.red);
                }
            }
        };
        SwingUtilities.invokeLater(doRun);
    }

    @Override
    public void run() {
        while (runProcess) {
            try {
                Thread.sleep(5000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            executor.execute(new TableIcon.MyTask("ChangeIconLabel")); // non on EDT
        }
    }

    private void setIconLabel() {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                String text = "";
                dateRun = new java.util.Date();
                long tme = dateRun.getTime();
                long she = periodicMonitor.getDelay(TimeUnit.SECONDS);
                dayCount = (int) (she / (24 * 60 * 60));
                hourCount = (int) (she / (60 * 60));
                minuteCount = (int) (she / (60));
                secondCount = (int) she;
                int hourss = hourCount;
                int minutess = minuteCount;
                if (dayCount > 0) {
                    hourCount -= (dayCount * 24);
                    minuteCount -= ((dayCount * 24 * 60) + (hourCount * 60));
                    secondCount -= (minutess * 60);
                    //System.out.println(" Days : " + dayCount + "  ,Hours : " + hourCount + "  , Minutes : " + minuteCount + "  , Seconds : " + secondCount);
                    text = ("  " + dayCount + " Days  " + hourCount + " h : " + minuteCount + " m : " + secondCount + " s");
                } else if (hourCount > 0) {
                    minuteCount -= ((hourss * 60));
                    secondCount -= (minutess * 60);
                    //System.out.println(" Hours : " + hourCount + "  , Minutes : " + minuteCount + "  , Seconds : " + secondCount);
                    text = ("  " + hourCount + " h : " + minuteCount + " m : " + secondCount + " s");
                } else if (minuteCount > 0) {
                    secondCount -= (minutess * 60);
                    //System.out.println(" Minutes : " + minuteCount + "  , Seconds : " + secondCount);
                    text = ("  " + minuteCount + " m : " + secondCount + " s");
                } else {
                    //System.out.println(" Seconds : " + secondCount);
                    text = ("  " + secondCount + " s");
                }
                tme += she * 1000;
                ImageIcon myIcon = (ImageIcon) table.getModel().getValueAt(count, 0);
                String lbl = "Row at :  " + count + "  Remains : " + text;
                myLabel.setIcon(myIcon);
                myLabel.setText(lbl);
                count++;
                if (count > 2) {
                    count = 0;
                }
            }
        });
    }

    public static void main(String[] args) {
        TableIcon frame = new TableIcon();
        frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
        frame.setLocation(150, 150);
        frame.pack();
        frame.setVisible(true);
    }

    private class MyTask extends SwingWorker<Void, Integer> {

        private String str;
        private String namePr;

        MyTask(String str) {
            this.str = str;
            addPropertyChangeListener(new SwingWorkerCompletionWaiter(str, namePr));
        }

        @Override
        protected Void doInBackground() throws Exception {
            if (str.equals("GetCurrTime")) {
                distAppInfo();
            } else if (str.equals("ChangeIconLabel")) {
                setIconLabel();
            } else if (str.equals("StartShedule")) {
                changeTableValues();
            }
            return null;
        }

        @Override
        protected void process(List<Integer> progress) {
            //System.out.println(str + " " + progress.get(progress.size() - 1));
        }

        @Override
        protected void done() {
            if (str.equals("GetCurrTime")) {
                changeLabelColor();
            } else if (str.equals("ChangeIconLabel")) {
                //setIconLabel();
            } else if (str.equals("StartShedule")) {
                //changeTableValues();
            }
        }
    }

    private class SwingWorkerCompletionWaiter implements PropertyChangeListener {

        private String str;
        private String namePr;

        SwingWorkerCompletionWaiter(String str, String namePr) {
            this.str = str;
            this.namePr = namePr;
        }

        SwingWorkerCompletionWaiter(String namePr) {
            this.namePr = namePr;
        }

        @Override
        public void propertyChange(PropertyChangeEvent event) {
            if ("state".equals(event.getPropertyName()) && SwingWorker.StateValue.DONE == event.getNewValue()) {
                System.out.println("Thread Status with Name :" + str + ", SwingWorker Status is " + event.getNewValue());
            } else if ("state".equals(event.getPropertyName()) && SwingWorker.StateValue.PENDING == event.getNewValue()) {
                System.out.println("Thread Status with Mame :" + str + ", SwingWorker Status is " + event.getNewValue());
            } else if ("state".equals(event.getPropertyName()) && SwingWorker.StateValue.STARTED == event.getNewValue()) {
                System.out.println("Thread Status with Name :" + str + ", SwingWorker Status is " + event.getNewValue());
            } else {
                System.out.println("SomeThing Wrong happends with Thread Status with Name :" + str);
            }
        }
    }
}

abstract class AccurateScheduledRunnable implements Runnable {

    private ScheduledFuture<?> thisThreadsMonitor;

    public void setThreadMonitor(ScheduledFuture<?> monitor) {
        this.thisThreadsMonitor = monitor;
    }

    protected long getExecutionTime() {
        long delay = -1 * thisThreadsMonitor.getDelay(TimeUnit.MILLISECONDS);
        return delay;
    }
}
...