並行プログラミング(Concurrent Programming)


並行プログラミングの目的

並行プログラミングの目的は二種類ある.一つは並列計算機の上で高速に計 算をするためのものである.もう一つは,逐次計算機上で動かすことを想定し ているが,自然に書くためである.Java における並列プログラミングは主に 後者を目的としている.

Java言語ではスレッド(thread, 「thread of control」(制御の糸)の短縮形) という単位で並列性を制御する.論理的に並列というのは,本当に並列に動く のではなく,一つの計算機上で時分割(time-sharing)に動くことも許す(とい うより,大多数の実装ではそうなっている)ことを意味する.スーパーコンピュー タ用の FORTRAN処理系などでは,for ループを書くと自動的に並列化するもの もあるが,Javaはこれらと違って明示的な(explicitな)並列実行の指定をおこ なっていて,スレッドを生成したところでのみ並列性が現われる.

Unix上のC言語でもプロセス(あるいはOSのスレッド)を生成して,並行プログ ラムを書くことができるが,Java言語ではライブラリではなく言語の中核部分 に並行プログラムを支援するための仕組みを入れているので,書きやすいし, OSに依存しない書き方ができる.

論理的に並列に書くと自然に書けるプログラムは次のようなものである.


新しいスレッドを取得する方法

Javaでは新しい制御のスレッドを手に入れる方法が2つある. 両方の方法で,スレッドを作るプログラムのサンプルを見てみよう.
public class ThreadSample {
  public static void main(String [] args){
    ExtnOfThread t1 = new ExtnOfThread();
    t1.start();

    Thread t2=new Thread(new ImplOfRunnable());
    t2.start();
  }
}
class ExtnOfThread extends Thread{
  public void run(){
    System.out.println("Extension of Thread running");
    try{sleep(1000);}
    catch(InterruptedException ie){return;}
    System.out.println("Extension of Thread exiting");
  }
}

class ImplOfRunnable implements Runnable{
  public void run(){
    System.out.println("Implementation of Runnable running");
    // Threadのサブクラスではないので,下のようには書けない.
    //     try{sleep(1000);}
    //catch(InterruptedException ie){return;}
    System.out.println("Implementation of Runnable exiting");
  }
}
実行結果は以下のようになるだろう.
Extension of Thread running
Implementation of Runnable running
Implementation of Runnable exiting
Extension of Thread exiting
スレッドを呼び出す側は,Thread オブジェクトを作って,そのメソッド startを呼び出す.メソッド start はスレッドが動き出すための準備をした後 で,run を呼び出す.これが,スレッドの実行の主体となる.runメソッドの 実行を終えると,スレッドの実行も終わる.

Threadクラスにはrun メソッド以外に,スレッドの実行を制御するための start,stop,sleep(ミリ秒単位での休止) というメソッドがある.Runnableイ ンタフェースで,これらのメソッドを使うには,スレッドを生成するときに記 憶しておくか,あるいはスレッドを実行中に ThreadクラスのcurrentThreadメ ソッドでスレッドを取得する必要がある.さきほどのプログラムを書き直して みる.

public class ThreadSample1 {
  public static void main(String [] args){
    ExtnOfThread1 t1 = new ExtnOfThread1();
    t1.start();

    Thread t2=new Thread(new ImplOfRunnable1());
    t2.start();
  }
}
class ExtnOfThread1 extends Thread{
  public void run(){
    System.out.println("Extension of Thread running");
    try{sleep(1000);}
    catch(InterruptedException ie){return;}
    System.out.println("Extension of Thread exiting");
  }
}

class ImplOfRunnable1 implements Runnable{
  public void run(){
    System.out.println("Implementation of Runnable running");
    Thread th=Thread.currentThread();
    try{th.sleep(1000);}
    catch(InterruptedException ie){return;}
    System.out.println("Implementation of Runnable exiting");
  }
}
このプログラムの実行結果は,
Extension of Thread running
Implementation of Runnable running
Extension of Thread exiting
Implementation of Runnable exiting
のようになる.
複数のスレッドが同じデータを操作する時は,予測の付かない動きをするこ ともある.次のプログラムは,10個のスレッドが,圧力計の値を見て,安全な ら圧力を加えるというプログラムである.
class pressure extends Thread{
  void RaisePressure(){
    if(p.pressureGauge < p.safetyLimit-15){
      try{sleep(100);}
      catch(Exception e){}
      p.pressureGauge+=15;
    }
  }
  public void run(){
    RaisePressure();
  }
}

public class p {
  static int pressureGauge=0;
  static final int safetyLimit=20;
  
  public static void main(String[] args){
    pressure [] p1=new pressure[10];
    int i;
    for(i=0;i<10;i++){
      p1[i]=new pressure();
      p1[i].start();
    }
    try{
      for(i=0;i<10;i++) p1[i].join();
    }
    catch (Exception e){}
    System.out.println("gauge reads "+pressureGauge+", safe limit is 20");
  }
}
これを実行すると,
gauge reads 150, safe limit is 20
となり,範囲を超えてしまうだろう.これを防ぐために,Java言語では相互排 他(mutual exclusion)を実現するためのsynchronized というキーワードを用 意してある.この使い方は,メソッドにつける.ブロックにつけるなどいろい ろある.先ほどの例では,
class pressure1 extends Thread{
  static synchronized void RaisePressure(){
    if(p1.pressureGauge < p1.safetyLimit-15){
      try{sleep(100);}
      catch(Exception e){}
      p1.pressureGauge+=15;
    }
  }
  public void run(){
    RaisePressure();
  }
}

public class p1 {
  static int pressureGauge=0;
  static final int safetyLimit=20;
  
  public static void main(String[] args){
    pressure1 [] p1=new pressure1[10];
    int i;
    for(i=0;i<10;i++){
      p1[i]=new pressure1();
      p1[i].start();
    }
    try{
      for(i=0;i<10;i++) p1[i].join();
    }
    catch (Exception e){}
    System.out.println("gauge reads "+pressureGauge+", safe limit is 20");
  }
}
のようにRaisePressureに static synchronized というキーワードを付けると, 同じクラスのオブジェクトが,RaisePressureを同時に実行するのを防ぐこと ができる.
スレッドを使ったアプレットのアニメーション
import java.applet.*;
import java.awt.*;
public class Test15 extends Applet implements Runnable{
// スレッドの宣言
  public Thread th=null;
  int x=50,y=20,dx=4,dy=0;
  public void paint(Graphics g){
    g.setColor(Color.white);
    g.fillRect(0,0,200,200);
    g.setColor(Color.black);
    g.drawLine(0,150,200,150);
    g.setColor(Color.red);
    g.drawString("Click me", x, y);
  }
// Runnable な Applet はまず, start メソッドが呼ばれる
  public void start(){
// スレッドができていない時はここで作成する
    if(th==null){ th=new Thread(this); th.start();}
  }
// stop メソッドを作っておかないと, WWWブラウザで別のページに行っても動き続けてしまうことがある. 
  public void stop(){
    if(th!=null){ th.stop(); th=null;}
  }
// Runnable な Appletでは, run メソッドが実行の主体となる
  public void run(){
    while(th.isAlive()){
      dy=dy+2;x=x+dx;y=y+dy;
      if(x<10){	x=10+(10-x); dx= -dx; }
      else if (x>150){ x=150-(x-150); dx= -dx; }
      if(y>150){ y=150-(y-150); dy= -dy;}
// 画面の更新. これを忘れると変更の結果が表示されない
      repaint();
//  Threadクラス  のsleep メソッドで ミリ秒単位の sleep(休止) を指定できる.
      try { Thread.sleep(200); }
      catch(InterruptedException e){}
    }
  }
}
アプレットの中でアニメーション(リアルタイムゲームを含む)をするのは結構 面倒である.まず,アプレットにはmainがない(書いても呼ばれない)ので,明 示的にスレッドを作る必要がある.アプレットは Applet クラスのサブクラス で作らなくてはいけないので,Runnable インタフェースを継承することになる.

mainがないのでどこで,実行のためのスレッドを作るかということだが, initメソッドの中で作るのも可能だが,一度スレッドを作るとWWWブラウザで 他のページに移っても動き続けるというのはやっかいなので,アプレットのあ るページに移動した際に呼ばれる start メソッドの中で作って,アプレット のあるページから抜けた際に呼ばれる stop メソッドの中で終了させるのが一 般的である.以前の BallGame.java をアプレットとして書き直したのが以下 のプログラムである.

// <applet code=BallGameApplet width=300 height=600></applet>
import java.applet.*;
import java.awt.*;
import java.awt.event.*;

public class BallGameApplet extends Applet implements KeyListener, MouseListener, Runnable{
    // ボールの座標,速度のx, y成分の宣言
  int ball_x=100,ball_y=100,ball_vx=16,ball_vy=12;
    // ボールの大きさ
  int ball_size=20;
    // 画面の幅,高さ
  int width=300, height=600;
    // バーの座標
  int bar_x=0, bar_y=550;
    // バーの速度
  int bar_vx=0;
    // バーの幅,高さ
  int bar_width=60,bar_height=10;
    // BallGameクラスのコンストラクタ
    // アニメーションを行うためのスレッド
  public Thread th=null;
    // WWWブラウザがアプレットを含むページに来たときに呼ばれる.
  public void init(){
    System.out.println("init is called");
    addKeyListener(this);
    addMouseListener(this);
    requestFocus();
  }
  public void start(){
    System.out.println("start is called");
      // スレッドができていない時はここで作成する
    if(th==null){ th=new Thread(this); th.start();}
  }
  public void stop(){
    System.out.println("stop is called");
      // スレッドがある時はスレッドを止める
    if(th!=null){ th.stop(); th=null;}
  }
    // 上のstartメソッドの中で,th.start()が呼ばれるとスレッドからこの
    // メソッドが呼ばれる.
  public void run(){
    for(;;){
        // 0.1秒(100ミリ秒)スリープする
      try { th.sleep(100); }
      catch(Exception e){}
        // ボール,バーの移動をおこなう
      timeTick();
    }
  }
  void timeTick(){
      // バーの移動
    bar_x=bar_x+bar_vx;
      // 左端から飛び出そうになったら左端に合わせる
    if(bar_x<0) bar_x=0;
      // 右端から飛び出そうになったら右端に合わせる
    else if(bar_x+bar_width >width) bar_x=width-bar_width;
      // ボールの移動.古い座標を保存
    int old_x=ball_x;
    int old_y=ball_y;
      // 速度に従って,次の座標を決定
    ball_x=ball_x+ball_vx;
    ball_y=ball_y+ball_vy;
      // 左端から飛び出そうになったら,反射させる
    if(ball_x<0){
      ball_x= -ball_x;
      ball_vx= -ball_vx;
    }
      // 右端から飛び出そうになったら,反射させる
    else if(ball_x >width-ball_size){
      ball_x=(width-ball_size)-(ball_x-(width-ball_size));
      ball_vx= -ball_vx;
    }
      // 上端から飛び出そうになったら,反射させる
    if(ball_y<0.0){
      ball_y= -ball_y;
      ball_vy= -ball_vy;
    }
      // バーのある線を通過しそうになったら,
    else if(ball_y >bar_y && old_y<=bar_y){
        // バーのある線を横切るX座標を計算
      int x=old_x+(ball_x-old_x)*(bar_y-old_y)/(ball_y-old_y);
        // バーと接触している場合は反射させる.
      if(bar_x <= x && x<=bar_x+bar_width){
        ball_y=bar_y-(ball_y-bar_y);
        ball_vy= -ball_vy;
      }
    }
      // 下端から飛び出そうになったら,反射させる
    else if(ball_y > height){
      ball_vy=-ball_vy;
    }
      // 再表示
    repaint();
  }
  Image offScreenImage;
  Graphics offScreenGraphics;
  public void update(Graphics g){
    if(offScreenImage==null){
      // 現在のフレームのサイズを求める
      Dimension d=getSize();
      // オフスクリーンイメージを作成
      offScreenImage=createImage(d.width,d.height); 
      // オフスクリーンイメージに描画するための Graphics オブジェクト
      offScreenGraphics=offScreenImage.getGraphics(); 
    }
    paint(offScreenGraphics); // 次の画面のイメージを作る.
    g.drawImage(offScreenImage,0,0,this); // イメージを本物のスクリーンに書き込む
  }
  public void paint(Graphics g){
      // 描画色を黒にする.
    g.setColor(Color.black);
      // 全体を塗り潰す
    g.fillRect(0,0,width,height);
      // 描画色を白にする.
    g.setColor(Color.white);
      // 警告文字列を(0,200)の点を左下にして描く
    g.drawString("Not a game, but a excercise in CP1(Wed/tanaka)",0,200);
      // 描画色を赤にする.
    g.setColor(Color.red);
      // ボールを描く
    g.fillOval(ball_x,ball_y,ball_size,ball_size);
      // オフスクリーンイメージへの描画色を青にする.
    g.setColor(Color.blue);
      // バーを描く
    g.fillRect(bar_x,bar_y,bar_width,bar_height);
  }
    // キーが押された時にこのメソッドが呼ばれる.
  public void keyReleased(KeyEvent e){}
  public void keyTyped(KeyEvent e){}
  public void keyPressed(KeyEvent e){
    System.out.println("keyPressed("+e+")");
    int key=e.getKeyChar();
      // 'h' のキーが押された時は,バーの移動速度を -10に
    if(key=='h'){
      bar_vx= -10;
    }
      // 'l' のキーが押された時は,バーの移動速度を 10に
    else if(key=='l'){
      bar_vx=10;
    }
      // 'j' のキーが押された時は,バーの移動速度を 0に
    else if(key=='j'){
      bar_vx=0;
    }
    else if(key=='q'){
      System.exit(0);
    }
  }
  public void mouseReleased(MouseEvent e){}
  public void mousePressed(MouseEvent e){
    System.out.println(e);
  }
  public void mouseClicked(MouseEvent e){}
  public void mouseEntered(MouseEvent e){}
  public void mouseExited(MouseEvent e){}
}
ただし,困ったことに,このプログラムはJDK 1.1の appletviewerで実行させると, キーボード入力を受け付けない. アプレット
次に進む