CodeGym /コース /JAVA 25 SELF /非同期 I/O におけるエラー処理と操作のキャンセル

非同期 I/O におけるエラー処理と操作のキャンセル

JAVA 25 SELF
レベル 56 , レッスン 3
使用可能

1. 非同期処理におけるエラー処理

同期コードでは単純です。ファイルが見つからない、またはアクセス権がない場合は、その場で try-catch で例外を捕捉します。非同期コードでは、特にコールバック (CompletionHandler) を使う場合、エラーはメソッドが終了した後に、スレッドプールの内部で発生することがあります。これを正しく処理しないと、プログラムは予測不能に振る舞い、データが「ひっそり」失われたり、アプリ全体がクラッシュしたりすることがあります。

エラーは CompletionHandler にどのように渡されますか?

インターフェース CompletionHandler<V, A> には 2 つのメソッドがあります:

  • completed(V result, A attachment) — 操作が成功したときに呼び出されます。
  • failed(Throwable exc, A attachment) — エラーが発生したときに呼び出されます。

使用例:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncErrorDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("nonexistent.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("正常に " + result + " バイト読み取りました");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("ファイルの読み取りエラー: " + exc.getMessage());
                    // ログ記録したり、ユーザーへ通知したり、例外をさらに伝播させたりできる
                }
            });
        } catch (IOException ex) {
            System.out.println("ファイルを開く際のエラー: " + ex.getMessage());
        }

        // 非同期操作が完了する時間を与える(実アプリでは CountDownLatch などの仕組みを使用する)
        Thread.sleep(500);
    }
}

ここで何が起きているか?

  • ファイルが存在しない場合、failed メソッドが該当する例外 (NoSuchFileException) とともに呼ばれます。
  • 操作が成功裏に終了した場合は completed が呼ばれます。

代表的なエラーの例

  • ファイルが見つからない: NoSuchFileException
  • アクセスなし: AccessDeniedException
  • 読み取り/書き込みエラー: IOException の各種サブクラス
  • バッファの問題: BufferOverflowException, BufferUnderflowException

ロギングとユーザーへの通知

非同期コールバックでのエラーは慌てる必要はありませんが、何も起きなかったふりをするべきでもありません。よいプラクティスはエラーをログに記録すること(例: Logger)、そしてユーザーにとって重要な場合はメッセージ表示や UI のハンドラー呼び出しを行うことです。

ロギングの例:

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
    System.err.println("非同期操作のエラー: " + exc);
    exc.printStackTrace();
}

本番コードでは System.err ではなく(たとえば java.util.loggingLog4j などの)適切なロガーを使用してください。

2. 非同期操作のキャンセル

いつキャンセルが必要になるか?

非同期タスクを処理の途中で止める必要が生じることがあります。たとえば、ユーザーが方針を変えてファイルのダウンロード中に「キャンセル」を押した場合。あるいはウィンドウが閉じられ、その操作がもはや意味を持たなくなった場合。また、アプリ終了時に丁寧にリソースを解放したい場合もあります。

そのような状況のために、Java の非同期 I/O は Future によるキャンセルをサポートします。実行中のタスクを任意の時点で中断し、無駄なリソース消費を避けられます。

Future を使って操作をキャンセルするには?

AsynchronousFileChannelreadwrite メソッドは Future<Integer> を返します。このオブジェクトには cancel(boolean mayInterruptIfRunning) メソッドがあります。

例: 非同期読み取りのキャンセル

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // 少し待ってから操作をキャンセルします
            Thread.sleep(100);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("読み取り操作をキャンセルしました!");
            } else {
                System.out.println("操作をキャンセルできませんでした(すでに完了している可能性があります)");
            }
        }
    }
}

重要な注意点:

  • キャンセルは、まだ完了していない操作に対してのみ有効です。
  • 操作がすでに終了している場合はキャンセルできません。
  • キャンセル後にその Future に対して get() を呼ぶと、CancellationException が投げられます。

いつ操作をキャンセルできないか?

タスクがすでに終了している場合――成功でも失敗でも――それを止めることはできません。手遅れです。

また、すべての実装が OS レベルで本当に操作を中断できるわけではありません。たとえば一部のファイルシステムでは「キャンセル」は名目上だけで、操作自体は継続し、結果を単に無視するだけという場合があります。

3. 実践: エラー処理とキャンセル

例 1: 存在しないファイルの読み取り時のエラー処理

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.io.IOException;

public class AsyncErrorExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("no_such_file.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("操作は正常に完了しました");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("ファイル読み取りエラー: " + exc.getClass().getSimpleName() + " - " + exc.getMessage());
                }
            });
        } catch (IOException ex) {
            System.out.println("ファイルを開く際のエラー: " + ex.getMessage());
        }

        Thread.sleep(500);
    }
}

コンソールには何が表示されるか?

ファイルを開く際のエラー: no_such_file.txt

または、開くのではなく読み取り時にエラーが発生した場合:

ファイル読み取りエラー: NoSuchFileException - no_such_file.txt

例 2: 長時間の操作をキャンセルし、正しく終了する

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 10 MB

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // 50 ミリ秒後に操作をキャンセル(実験のため)
            Thread.sleep(50);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("読み取り操作はキャンセルされました!");
            } else {
                System.out.println("操作をキャンセルできませんでした(おそらく既に完了しています)");
            }

            try {
                // 結果を取得してみます(CancellationException が送出されます)
                future.get();
            } catch (java.util.concurrent.CancellationException ex) {
                System.out.println("CancellationException を捕捉: 操作は実際にキャンセルされました。");
            }
        }
    }
}

4. ベストプラクティス: 正しく行うには

エラー時でも必ずリソースを解放する

try-with-resources を使用してチャネルを自動的にクローズしましょう:

try (AsynchronousFileChannel channel = /* open channel */ null) {
    // ...
}

CompletionHandler を使う場合は、すべての操作が完了したらチャネルを閉じることを忘れないでください。特に複数の非同期操作を続けて実行する場合に重要です。

UI/メインスレッドをブロックしない

非同期操作はメインスレッドをブロックしないためのものです。UI スレッドで future.get() を呼ばないでください。そうすると非同期性の意味がなくなります。

すべてのエラーをログに記録する

CompletionHandler では必ず failed メソッドを実装し、すべての例外を記録(あるいは上位に伝播)してください。

プログラム終了前にすべての操作の終了を確認する

操作が終わる前にプログラムが終了すると結果が失われる可能性があります。コンソールのデモでは Thread.sleep(500) を使うこともありますが、実アプリでは CountDownLatchCompletableFuture などの同期機構を使用してください。

キャンセルを忘れない

操作が不要になった場合(例: ユーザーがウィンドウを閉じた)、Future.cancel でキャンセルしましょう。これによりリソースを節約し、アプリの応答性が向上します。

5. 非同期 I/O におけるエラー処理とキャンセルでのよくある間違い

エラー No.1: failed メソッドを CompletionHandler で無視する。
エラー処理を実装しないと、アプリは予測不能に挙動し、エラーが「消え」、ユーザーはなぜ何も起きないのか分からないままになります。

エラー No.2: 操作完了後にチャネルを閉じない。
AsynchronousFileChannel を閉じ忘れると、リソースリークや OS 上でのファイルロックにつながります。

エラー No.3: メインスレッドで非同期操作の結果を待つ。
UI スレッドで future.get() を呼ぶと、UI がフリーズし、非同期化の利点が失われます。

エラー No.4: すでに完了した操作をキャンセルしようとする。
cancel() を遅すぎるタイミングで呼ぶと、すでに操作が完了しておりキャンセルは効きません。致命的ではありませんが、デバッグ時に誤解のもとになります。

エラー No.5: キャンセル結果を確認しない。
cancel() を呼んだのに戻り値を確認せず、get() 呼び出し時の CancellationException も処理しないと、プログラムが落ちたり不安定に動作したりします。

エラー No.6: エラーやキャンセル時にリソースを解放しない。
エラーやキャンセル後にチャネルを閉じないと、リークやファイルロックが発生する可能性があります。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION