Threadを用いて長大ループの途中でキャンセルする

多数回ループを持つプログラムの処理では、途中で中止したくなることが多い。そのための仕組みを考える。ループを持つプロセスと並行にループを制御する変数の値を変えるボタンを配置し、そのループプロセスはループが回るたびに、制御変数の値により、ループの実行・中止を判断してループを回す。
問題として浮上したのは、次の点である。長大ループの個々のループの処理時間がある程度長い場合には、ループが回るごとに制御変数がキャンセルシグナルを伝えることができるが、個々のループはごく短時間である場合には、キャンセルシグナルを受け付けにくい点である。下記のソースは、ソース単体で起動し、実行開始ボタンとキャンセルボタンを表示する。実行開始をすると、指定時間間隔ごとに経過時間を示すカウンタ表示が更新される。また、キャンセルにより、処理は中段し、カウンタ表示も閉じられる。また、再実行に備えて、画面表示も変更する。キャンセルボタンが押されない場合には、当初予定のループ回数を終了して、終了メッセージを表示する。このようなケースには、長大プロセス側で、定期的に(100万回ループの場合には、100ループごとにプロセス保留の時間などを挿入するなどして、キャンセルシグナルを受け付けやすくするなどの工夫が考えられるが、試していないので、実装上の問題の存在は否定できない。
その他の技術的な問題としては親プロセス(実行・キャンセルボタンを有するフレーム)のexitによって、きちんとThreadプロセスを停止する点で不安が残ることである。今回は、System.exit(0)でJavaプロセスを最上流から終了することとしてあるが、アプリケーション実行上は、親とその子孫プロセスのみ一括中止したいことの方が普通であるから、実際にそぐわない。おそらく"Thread"という仕組みの目的から言って、親-子孫一括中止は組み込まれているのだと思われる。
参考サイト

  1. Javaによる画面(GUI)作成 Tips
  2. JavaでHello World > スレッド編

以下のお二人からの情報提供に感謝します。
kuma_the_sealさん、ishikuraさん

/*
 * 作成日: 2005/10/30
 *
 * TODO この生成されたファイルのテンプレートを変更するには次へジャンプ:
 * ウィンドウ - 設定 - Java - コード・スタイル - コード・テンプレート
 */
	import java.awt.*;
	import java.awt.event.*;

	import javax.swing.*;
	import java.lang.Object;
	
	public class CancelTest extends JFrame{
		int endFlg=0;//
	    //主処理
	    public static void main(String ar[]){
	    	CancelTest sample = new CancelTest();
	    	
	    }
	    //コンストラクタ
	    public CancelTest(){
	    	
	        // フレームを作成
	        JFrame f=new JFrame("Controller");
	        
	        //ボタン・パネルをフレームに搭載
	        JButton b01=new JButton("Start");
	        JButton b02=new JButton("Cancel");
	        b01.setActionCommand("Start");
	        b01.addActionListener(new PushButtonActionListener(f));
	        b02.setActionCommand("Cancel");
	        b02.addActionListener(new PushButtonActionListener(f));
	        
	        JPanel pb=new JPanel();
	        pb.add(b01);
	        pb.add(b02);
	        
	        f.getContentPane().add(pb);
	        
	        // フレームを表示
	        f.setLayout(new FlowLayout(FlowLayout.CENTER, 2, 2));
	        f.setLocation(50, 50);
	        f.setSize(200,100);
	        f.setVisible(true);
	        
	        //フレームを閉じたらアプリケーション全体を終了
	        f.addWindowListener(new WindowAdapter(){
	         	public void windowClosing(WindowEvent e) {
	         		System.exit(0);
	         	}
	         });
	    }
	    
	    //ボタンクリック時のアクションリスナ
	    private class PushButtonActionListener implements ActionListener{
	        JFrame f = null;
	        public PushButtonActionListener(JFrame af) {
	               this.f = af;
	        }
	        //"Start"ボタンが押されたら、endFlgを0(MyThreadを実行する条件)にして、MyThreadを開始する
	        //"Cancel"ボタンが押されたら、endFlgを1(MyThreadを実行しない条件)にする。
	        //その結果、MyThreadのループが止まる
	        public void actionPerformed(ActionEvent ae){
	            if(ae.getActionCommand()=="Start"){
	            	endFlg=0;
	            	MyThread mt = new MyThread();
	            	mt.start();
	            	
	            }else if(ae.getActionCommand()=="Cancel"){
	            	endFlg = 1;
	            	
	            }
//	          キャンセル後、メッセージを出し、再スタートを促す
            	JLabel jl = new JLabel("You can restart");
            	JPanel jp = new JPanel();
            	jp.add(jl);
            	f.getContentPane().add(jp);
            	f.setVisible(true);
	        }    
	    }
	    
	    class MyThread extends Thread{
	    	//コンストラクタ
	        MyThread(){
	        }

	        public void run(){
	        	
	        	//フレームを立ち上げる
		        JFrame fa = new JFrame("Messages");
		        //フレーム上にラベルを作成しそこに表示文字列を指定し、フレーム上に配置して表示条件を決めて
		        //表示させる
		        String mss1 = "";
		        JLabel dir1 = new JLabel(mss1);
		    	JPanel pdir1;
		    	pdir1 = new JPanel();
		    	pdir1.add(dir1);
		    	fa.getContentPane().add(pdir1);		    	
		    	fa.setLocation(100,200);
		        fa.setSize(400,100);
		        fa.setVisible(true);
		        //"Controller"フレームのボタンによって決められる実行条件パラメタendFlgが0である限り、
		        //開始からの秒数を刻み、ラベルに表示する
	            int count = 0;
	          	//キャンセル命令を与えたいループ処理の実行の可否をendFlg条件で制御することで
	            //任意のループ処理にキャンセルオプションを付与することができる
	            //ただし、1回のループの処理時間が短い場合には、キャンセルを受け付けが
	            //うまくいかないことがある
	            //したがって、キャンセル命令を与えたいループが、1つ1つは短時間だが、
	            //その数が非常に多いために長時間化する処理の場合には
	            //この方法では、うまくキャンセルが入れられない
	            
	            int num_max_loop=10;
	            int num_looped=0;
	            for(int i=0;i<num_max_loop;i++){
	            	if(endFlg==1){
	            		//ControllerのCancelボタンがクリックされたら、秒数表示フレームを消す
		            	fa.dispose();
	            	}else if(endFlg==0){
		            	//100ミリ秒間スレッドを保留する
		            	//この時間保留は、通常のループ処理キャンセルでは不要
		            	try {
		    	  	        sleep(1000);
		    	  	      } catch (InterruptedException e) {

		    	  	      }
		    	  	    
	            		mss1 = "0." + i + "秒";
	            		fa.getContentPane().removeAll();
	            		dir1 = new JLabel(mss1);
	            		pdir1 = new JPanel();
	    		    	pdir1.add(dir1);
	    		    	fa.getContentPane().add(pdir1);
	    		    	fa.setVisible(true);
	    		        num_looped++;//ループ回数のカウンタ
	            	}
	            	 
	            }
	            if(num_looped==num_max_loop){
	            	mss1 = "Process was completed without cancelation";
	        		fa.getContentPane().removeAll();
	        		dir1 = new JLabel(mss1);
	        		pdir1 = new JPanel();
			    	pdir1.add(dir1);
			    	fa.getContentPane().add(pdir1);
			        fa.setVisible(true);
	            }
	        }
	    }
	}