mutex に関する考察

今日、社内のレビュがあって synchronized なあたりの理解というか説明が微妙だったのでこちらで理解できてる事を纏めておいて別途フォローしてみる方向でナニ。

サンプルコード

http://www.anddev.org/viewtopic.php?p=22820 の下に出てくるソレを流用。Activity 側が以下。

public class AudioRecorderPCM extends Activity {
     /** Called when the activity is first created. */
     @Override
     public void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.main);
          
          // Record 20 seconds of audio.
          Recorder recorderInstance = new Recorder();
          Thread th = new Thread(recorderInstance);
          recorderInstance.setFileName(new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.raw"));
          th.start();
          recorderInstance.setRecording(true);
          synchronized (this) {
          try {
               this.wait(20000);
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
          
          }
          recorderInstance.setRecording(false);
          try {
               th.join();
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
          
          
     }
} 

Recorder というクラスの定義が以下。

import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;


public class Recorder implements Runnable {
     private int frequency;
     private int channelConfiguration;
     private volatile boolean isPaused;
     private File fileName;
     private volatile boolean isRecording;
     private final Object mutex = new Object();

     // Changing the sample resolution changes sample type. byte vs. short.
     private static final int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;

     /**
      *
      */
     public Recorder() {
          super();
          this.setFrequency(11025);
          this.setChannelConfiguration(AudioFormat.CHANNEL_CONFIGURATION_MONO);
          this.setPaused(false);
     }

     public void run() {
          // Wait until we're recording...
          synchronized (mutex) {
               while (!this.isRecording) {
                    try {
                         mutex.wait();
                    } catch (InterruptedException e) {
                         throw new IllegalStateException("Wait() interrupted!", e);
                    }
               }
          }

          // Open output stream...
          if (this.fileName == null) {
               throw new IllegalStateException("fileName is null");
          }
          BufferedOutputStream bufferedStreamInstance = null;
          if (fileName.exists()) {
               fileName.delete();
          }
          try {
               fileName.createNewFile();
          } catch (IOException e) {
               throw new IllegalStateException("Cannot create file: " + fileName.toString());
          }
          try {
               bufferedStreamInstance = new BufferedOutputStream(
                         new FileOutputStream(this.fileName));
          } catch (FileNotFoundException e) {
               throw new IllegalStateException("Cannot Open File", e);
          }
          DataOutputStream dataOutputStreamInstance =
               new DataOutputStream(bufferedStreamInstance);
          
          // We're important...
          android.os.Process
                    .setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);

          // Allocate Recorder and Start Recording...
          int bufferRead = 0;
          int bufferSize = AudioRecord.getMinBufferSize(this.getFrequency(),
                    this.getChannelConfiguration(), this.getAudioEncoding());
          AudioRecord recordInstance = new AudioRecord(
                    MediaRecorder.AudioSource.MIC, this.getFrequency(), this
                              .getChannelConfiguration(), this.getAudioEncoding(),
                    bufferSize);
          short[] tempBuffer = new short[bufferSize];
          recordInstance.startRecording();
          while (this.isRecording) {
               // Are we paused?
               synchronized (mutex) {
                    if (this.isPaused) {
                         try {
                              mutex.wait(250);
                         } catch (InterruptedException e) {
                              throw new IllegalStateException("Wait() interrupted!",
                                        e);
                         }
                         continue;
                    }
               }
               
               bufferRead = recordInstance.read(tempBuffer, 0, bufferSize);
               if (bufferRead == AudioRecord.ERROR_INVALID_OPERATION) {
                    throw new IllegalStateException(
                              "read() returned AudioRecord.ERROR_INVALID_OPERATION");
               } else if (bufferRead == AudioRecord.ERROR_BAD_VALUE) {
                    throw new IllegalStateException(
                              "read() returned AudioRecord.ERROR_BAD_VALUE");
               } else if (bufferRead == AudioRecord.ERROR_INVALID_OPERATION) {
                    throw new IllegalStateException(
                              "read() returned AudioRecord.ERROR_INVALID_OPERATION");
               }
               try {
                    for (int idxBuffer = 0; idxBuffer < bufferRead; ++idxBuffer) {
                         dataOutputStreamInstance.writeShort(tempBuffer[idxBuffer]);
                    }
               } catch (IOException e) {
                    throw new IllegalStateException(
                         "dataOutputStreamInstance.writeShort(curVal)");
               }
               
          }

          // Close resources...
          recordInstance.stop();
          try {
               bufferedStreamInstance.close();
          } catch (IOException e) {
               throw new IllegalStateException("Cannot close buffered writer.");
          }
     }

     public void setFileName(File fileName) {
          this.fileName = fileName;
     }

     public File getFileName() {
          return fileName;
     }

     /**
      * @param isRecording
      *            the isRecording to set
      */
     public void setRecording(boolean isRecording) {
          synchronized (mutex) {
               this.isRecording = isRecording;
               if (this.isRecording) {
                    mutex.notify();
               }
          }
     }

     /**
      * @return the isRecording
      */
     public boolean isRecording() {
          synchronized (mutex) {
               return isRecording;
          }
     }

     /**
      * @param frequency
      *            the frequency to set
      */
     public void setFrequency(int frequency) {
          this.frequency = frequency;
     }

     /**
      * @return the frequency
      */
     public int getFrequency() {
          return frequency;
     }

     /**
      * @param channelConfiguration
      *            the channelConfiguration to set
      */
     public void setChannelConfiguration(int channelConfiguration) {
          this.channelConfiguration = channelConfiguration;
     }

     /**
      * @return the channelConfiguration
      */
     public int getChannelConfiguration() {
          return channelConfiguration;
     }

     /**
      * @return the audioEncoding
      */
     public int getAudioEncoding() {
          return audioEncoding;
     }

     /**
      * @param isPaused
      *            the isPaused to set
      */
     public void setPaused(boolean isPaused) {
          synchronized (mutex) {
               this.isPaused = isPaused;
          }
     }

     /**
      * @return the isPaused
      */
     public boolean isPaused() {
          synchronized (mutex) {
               return isPaused;
          }
     }

} 

で、上記 Recorder クラスで使われてる synchronized についてレビュ時に「どーゆーコトなのか説明して」なコメントがあり、自分的には若干微妙なコメント回答だった訳です。以下に整理してみます。

まず

最初からなぞってみます。onCreate メソドから。

          super.onCreate(savedInstanceState);
          setContentView(R.layout.main);
          
          // Record 20 seconds of audio.
          Recorder recorderInstance = new Recorder();
          Thread th = new Thread(recorderInstance);
          recorderInstance.setFileName(new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.raw"));
          th.start(); 

Recorder なオブジェクトと Thread なオブジェクトを生成して start メセジを Thread なオブジェクトに送ってます。run の先頭部分が以下。

          // Wait until we're recording...
          synchronized (mutex) {
               while (!this.isRecording) {
                    try {
                         mutex.wait();
                    } catch (InterruptedException e) {
                         throw new IllegalStateException("Wait() interrupted!", e);
                    }
               }
          }

レビュ時にオブジェクト単位で排他がかかる、という意味の発信をしたんですが、これは誤っていない模様 (?)。上記の例では少くとも

  • Activity の thread
  • Recorder の thread

が動作しているはず。ただしマルチスレッドとは言え、CPU は一つなのでどこかでコンテキスト切り替えをしながら処理が進んでいくと思われます。
で、上記の synchronized なブロックの処理中でコンテキストの切り替えが発生してしまうと都合が悪いので synchronized で囲んだブロックを mutex 排他してます。atomic operation というヤツですな。context switch は発生しない、と。
ただし onCreate から start された時点では isRecording は false なので (クラスの boolean なメンバは初期値 false)、いきなり wait します。mutex はステだし notify 呼ばれるまで止まったママ。
で、Activity 側の thread に実行コンテキストが移り (thread は二つだけではないと思われますが便宜的に限定してます)

          recorderInstance.setRecording(true);

がナニ。ちなみに Recording の isRecording 属性は volatile なキーワードが付いてます。なので mutex 取らないとアクセスできない、って自分ブログに記録が残っている。ちょっとこのエントリ記述が微妙ですな。スレッドが複数 start してるようなケイスではクラスの属性とかの変数をスレッド固有の領域にコピーして使用する模様。変数の値に不整合が起きる場合がある、という事で_コピーしないけんね_を宣言するのが volatile な模様。
# 本当かなぁ
で、setRecording メソドが以下か。

     public void setRecording(boolean isRecording) {
          synchronized (mutex) {
               this.isRecording = isRecording;
               if (this.isRecording) {
                    mutex.notify();
               }
          }
     } 

true がセットされたら notify で止まった thread をリスタートしても OK ですよ、とナニ。

まとめ

以下な理解で良いのだろうか。

  • synchronize な処理ブロック内ではコンテキスト切り替えはおきない
    • 要するに atomic
    • 要するにオブジェクトそのものがロックされるイメージ
  • volatile 修飾子付けたらその変数は thread 毎でコピー作るような最適化の範疇外となる
  • wait は引数略だと notify されるまで休眠

ダウト指摘十分覚悟してます。