読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

Javaの非同期処理を,シングルスレッドのようにシンプルにコーディングするための設計パターン (並列処理を逐次処理にする)

java 設計 マルチスレッド javascript プログラミング

重要なお知らせ:

この記事で公開した情報は,AndroidのMVCフレームワーク「Android-MVC」の機能の一部として取り込まれました。

より正確な設計情報や,動作可能な全ソースコードを閲覧したい場合,「Android-MVC」の公式ページより技術情報を参照してください。


AndroidのMVCフレームワーク - 「Android-MVC」
http://code.google.com/p/android-mvc-...


マルチスレッドの処理を,シングルスレッドであるかのようにコーディングしたい場合がある。

1番目の非同期タスクの処理結果を,2番目の非同期タスクが利用する場合など。


つまり,並列化されたタスクを,取扱い上は「逐次化」したいのだ。


まずは手っ取り早く,やりたい事をUMLで表現しよう。


「非同期タスクの連鎖」を実装する際,

しばしば下記のような「コールバックの入れ子」が生まれる。

その結果,メインの処理フローが見失われてしまう

「非同期タスクが連鎖している」という状況が,ソースコードから一目で伝わらないのだ。


この状況を改善するために,下記のようなシーケンス図に作り変えたい。

※ダイアグラム中で,矢印の先っぽが「同期呼び出し」と「非同期呼び出し」の違いを表している点に注意。


こうすれば,「非同期タスクが連鎖している」という状況が,ソースコードからも,クラス設計からも,一目で伝わるようになる。

メインの処理フローが,明示的にわかるようになるのだ。

そうすれば設計しやすくなり,実装しやすくなり,テストしやすくなり,保守しやすくなる。



加えて,後者の方がよりオブジェクト指向な設計だ。

  • 個々のタスク自身は,自分自身のタスクに専念すればよい。(Mind your own Business!)
  • どのタスクがどの順番で呼ばれるか?どう連携させるか?といった「制御フロー」に関する情報は,制御用の親クラスに任せる。(いわば,タスクマネージャ)


残念ながら,前者の「コールバックの入れ子が延々と続くので,処理を追いかけづらい」スタイルのコードのほうを,よく見かけるのでは。

この点は,JavaやJavaScriptなど,非同期タスクをサポートしているプログラミング言語であれば何でも問題になり得る*1


Javaの場合,言語レベルでsynchronizedブロックをサポートしているため,

  • スレッド間で「同期」を取れば簡単では?

と思うかもしれない。

しかし,synchronizedは乱用できるものではない。安易に使うとデッドロックを生むので,慎重な考察が必要になる。


ここでは,Javaで「synchronized」キーワードを使わずに,

複数の非同期タスクを「逐次的」にコーディングする方法を示す。


まず,「逐次的」とはどういうものか説明する。

そして,上述の「タスクマネージャ型」のシーケンス図に倣い,Javaでのマルチスレッド・プログラミングのコードをシンプルにするような設計技法を1つ示す。

  • (1)処理の「逐次化」とは?
  • (2)JavaScriptでの「逐次化」の具体例
  • (3)Javaでの「並列化」がスパゲッティコードを生みかねない例
  • (4)Javaで,複数の非同期タスクを「逐次化」する設計パターン例

(1)処理の「逐次化」とは?


「逐次化」とは,「並列化」の逆。

  • 並列プログラミング(Concurrent Programming)⇔ 逐次プログラミング(serial programming)
  • parallelな実行 ⇔ sequentialな実行

という風に,対照して使う。


逐次化を「シリアライズ」と呼ぶこともある。ただし「直列化」とは別の意味である。

かわりに「逐次化」「シーケンシャルな実行」と呼べば,誤解がなくて済む。

シリアライズ(逐次化)
http://ja.wikipedia.org/wiki/%E3%82%B...
ある一つの資源を、複数の主体が利用しようとするときに、それを調整(同期)して、一つの時点では一つの主体だけがそれを利用するようにすること。この意味では逐次化という訳語が用いられる。対義語は並列化である。

第一の意味の逐次化(ちくじか)は、主としてマルチスレッドプログラミングにおいて使われる用語で、この場合、ある資源が複数のスレッドから同時にアクセスを受け付けられるようになっていないとき、つまりマルチスレッド対応でないときに、マルチスレッド環境からもその資源を利用できるようにするために、同時のアクセス要求が起こったときには、それぞれのスレッドが順番にその資源を利用するように調整することをいう。

※あの有名なSerializableの意味とは異なるので注意。

(2)JavaScriptでの「逐次化」の具体例

非同期タスクを使うと,そのタスクが終了した時に呼ばれる処理を記述するために,コールバック関数を入れ子の形式でコーディングする必要がある。


非同期タスクが何個も連鎖するような場合,コールバックが「入れ子」になってしまう。それが嫌だ。

ソースから,処理の流れが追いづらくなる。


JavaScriptでは,Ajax(XmlHttpRequest)で非同期タスクを行なう事がきわめて一般的だ。

なので,こういった「非同期コールバックの入れ子問題」が発生する。


この問題を解決するライブラリとして,JavaScriptには「SJS」というのがある。

Javaの場合とほとんど事情は同じなので,下記の記事は一読の価値がある。

コールバック不要:Javascript に逐次プログラミングを取り戻す StratifiedJS
http://63-246-7-136.contegix.com/jp/articles/stratifiedjs

実行時ブロックを伴う処理には非同期 (asynchronous) プログラミングが必須ですが,Javascript でのプログラミングには通常,大量のコールバック生成と処理の転送を伴います。
これは開発者に対して,シーケンシャルなコードを継続渡し形式 (continuation passing style) に変換するための労力を強いることになります。

JS プログラマは SJS を使うことで,非同期処理コードを逐次実行的なスタイルで記述することができます。
コールバックの迷路に混乱させられることなく,コントロールフローを自身のソースコードに直接書き下せるのです。

・・・

プログラムに並列性を導入したならば,何らかの方法でそれを調整し統制する必要が生じます。
ことばを変えるならば,並列性を,一貫性を持ったひとつのストーリにまで “縮退” させる必要がある。

“手短に言うと,今日の並列プログラミングにおける主流技術(ロックと条件変数)には,根本的な欠陥があるのです。 [...]
ロックを基本としたプログラミングの根本的な欠点は,ロックと条件変数がモジュラプログラミングをサポートしていないことです。
ここで言う “モジュラプログラミング” とは,小さなプログラムを結合させて大規模なプログラムを構築する,そのプロセスを指しています。ロックはこれを不可能にするのです。”

問題が言い尽くされている。


「非同期タスクの連鎖」は,コーディングしづらいのだ。把握も変更もしづらい。

「実際に行いたいこと」と,「仕方なく実装したコールバック」との間に,乖離が大きい。

機能設計と処理設計の間にギャップが生じる。

クラス設計やコードが,機能の意図を伝えられない。


にも関わらず,悪い設計の見本(アンチパターン)として「コールバックの入れ子」が多用されてしまう。

何も考えず,既存のAPIやライブラリが提供する処理フローに従えば,当然そういうコードになる。

より良い設計を自ら生み出す事を忘れて(放棄して),見かけ上の「手軽さ」を採用してしまう。



この状況を変えたい。

ある非同期タスクの完了コールバックで,別の非同期タスクの起動処理を呼び出す,

という方法ではなく,全ての非同期タスクが1つの明示的な「ストーリー」を描くようにしたいのだ。

つまり,あたかも直列処理のようにコーディングしたい。



では,並列処理を直列処理のように見せかけるためには?


(3)Javaでの「並列化」がスパゲッティコードを生みかねない例

例えば,Javaでコーディングする際に,下記のようなライブラリが与えられているとしよう。

  • 「重い処理はメインではない別スレッドで行なうこと。その際はこのクラスを使うこと」
  • 「別スレッドの終了を待機するためにメインスレッド側で Thread#join() などを使ってはならない。メインスレッドが長時間停止してしまうのを防止するため。」

みたいな規約が,ベンダ側から一方的に課されているのだ。

package seqtest;

// ベンダーから提供された非同期タスクの基底クラス。
// 変更不可能。
abstract class VendorAsyncTask
{

    // 別スレッドで行ないたいメインの処理を記述する
    protected abstract void execOnNewThread();
    
    
    // 非同期タスクを開始する
    public void startAsyncTask()
    {
        // 新スレッド上で実行。startで開始したら,そのまま放置。
        Thread t = new Thread(){
            public void run()
            {
                execOnNewThread();
            }
        };
        t.start();
    }

}

ユーザは,ベンダが提供したこのクラスを使って,非同期処理を記述することになる。

複数の非同期タスクを実行したい場合,execOnNewThread() メソッド内では,次の非同期タスクの呼び出しも記述しなければならない。

コールバックの入れ子である。


この事態を解決するために,synchronized句を導入する手もある。

だがそうしてしまうと,処理レベルでデッドロックの危険がつきまとう事になり,設計の手間が増える。

もちろん,階層的に奥の方の,データ操作のレベルでは積極的にsynchronizedすべきだ。

しかし,処理のレベルで複数の非同期タスクを連鎖させる場合,もっと簡潔なアプローチがあるはずである。


そのアプローチ・設計パターンがわかれば,

synchronizedを言語レベルでサポートしない言語においても同様の応用がきく。


(4)Javaで,複数の非同期タスクを「逐次化」する設計パターン例

見るべきコードとして,最初に「理想形」のメイン処理から掲載する。

サンプルコード中で最も「こういうふうに書けたらなあ」と思う部分だからだ。

package seqtest;

import seqtest.*;


// メイン処理。
public class Main
{
    public static void main( String[] argv )
    {
        
        // 非同期タスクたちを順番に実行させる。
        
        new AsyncTasksRunner( new SequentialAsyncTask[]{
        
            // 下記に,逐次実行したい非同期タスクを列挙する。
            
            // 1番目の非同期タスク
            new HogeTask(){
                public boolean exec(){
                    
                    log("main外のスレッドで,1から100までの合計値を計算します。");
                    
                    // 計算実行
                    int sum = 0;
                    for( int i = 1; i < 101; i ++ )
                    {
                        sum += i;
                    }
                    log("計算終了。この値を別のタスクから参照可能になるよう保持。");
                    storeObject( "sum1", sum );
                    
                    return true;
                }
            }
            ,
            
            // 2番目の非同期タスク
            new HogeTask(){
                public boolean exec(){
                    
                    // 以前の非同期タスクから値を受け取ることができる
                    log("前の非同期タスクの実行結果は" + getDataFromRunner("sum1"));
                    
                    
                    log("main外のスレッドで,101から200までの合計値を計算します。");
                    
                    // 計算実行
                    int sum = 0;
                    for( int i = 101; i < 201; i ++ )
                    {
                        sum += i;
                    }
                    log("計算終了。この値を別のタスクから参照可能になるよう保持。");
                    storeObject( "sum2", sum );
                    
                    return true;
                }
            }
            ,
            
            // 3番目の非同期タスク
            new HogeTask(){
                public boolean exec(){
                    log("前の非同期タスクの実行結果は" + getDataFromRunner("sum2"));
                    log("前の前の非同期タスクの実行結果は" + getDataFromRunner("sum1"));
                    
                    return true;
                }
            }
        }).begin();
        
        
        System.out.println("main()が終了。");
    }
}


AsyncTasksRunnerという,非同期タスクの配列のコンテナみたいなオブジェクトを定義している。

そしてその中に,実行したい非同期タスクを「順番に」コーディング。

「順番に」=「逐次的に」というのがポイント。



「new AsyncTasksRunner( new SequentialAsyncTask[]{ 」の部分が,要点である。

タスクを表すオブジェクトを,コンテナに放り込んで,あとは begin() するだけ。


1タスクの完了時のコールバック関数を書く必要がない。

その次のタスクが自動的に受理されてゆくからである。

※Web屋さんであれば,上記のコードは prototype.js の Try.these() 関数にちょっと類似している,という点に気づくだろう。もっとも,Javaにはクロージャが無いので,手の込んだ仕組みが必要になるが…。



後続の非同期タスク内では,実行済みの非同期タスクの処理結果を参照できる,というのも大きな強み。

スレッド間での連携が容易になる。



このようなメインコードを記述可能にさせるためには,

既存のベンダー提供のクラスとも合わせて,どのようなクラス設計にすればよいか?


順番に解説する。



まず,非同期タスクの種類に応じて,自由にクラスを作る。

ここでは1種類だけだが,「SequentialAsyncTask」(逐次化可能な非同期タスク)を継承していることが条件。

package seqtest;

// カスタム非同期タスクの一種。
abstract class HogeTask extends SequentialAsyncTask
{
}

そして,その基底となる「逐次化可能な非同期タスク」を下記のように作成する。

package seqtest;

import seqtest.*;

import java.util.HashMap;


// 逐次化可能な非同期タスクの基底クラス
abstract class SequentialAsyncTask extends VendorAsyncTask
{
    // 呼び出し元のランナー
    protected AsyncTasksRunner parent = null;
    
    // タスクの実行結果
    private boolean task_execution_result = true;
    
    // タスク内の保持データ
    private HashMap<String, Object> hash = new HashMap<String, Object>();
    
    
    // 別スレッドで行ないたいメインの処理。継続の是非を返す。
    public abstract boolean exec();
    
    
    // メイン処理の実行をラップさせるメソッドとする
    @Override
    protected void execOnNewThread()
    {
        // メイン処理を実行
        task_execution_result = exec();
        log("別スレッドでのタスク実行を終了しました。実行結果は" + task_execution_result);
        
        // 呼び出し元に通知
        parent.onCurrentTaskFinished();
    }
    
    
    // ランナーにより,このタスクを開始
    public void kickByRunner( final AsyncTasksRunner parent )
    {
        // 呼び出し元のランナーをセット
        this.parent = parent;
        
        // 別スレッド上でタスクを開始
        startAsyncTask();
        log("別スレッドでのタスク実行を開始しました。");
    }
    
    
    // 次のタスクへ継続可能かどうかを判定
    public boolean tasksContinuable()
    {
        return task_execution_result;
    }
    
    
    // ------------- この非同期タスク内部で保持するデータ ------------
    
    
    // 任意のデータを1つ格納
    protected void storeObject( String key, Object val )
    {
        hash.put( key, val );
        log("タスク内で," + key + "というキーの値をハッシュに登録しました。");
    }
    
    
    // 全データを返す
    public HashMap<String, Object> getStoredObjects()
    {
        return hash;
    }
    
    
    // デバッグ用
    protected void log( String s )
    {
        System.out.println( "[DEBUG] " + this.getClass().getName() + " : " + s );
            // http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1313995243
    }
    
    
    // ------------- 別の非同期タスクと共有するデータ ------------
    
    
    // 任意のデータをランナー側から1つ取得
    protected Object getDataFromRunner( String key )
    {
        return parent.getDataByKey( key );
    }
    
    
}

このクラスが持っている機能は色々ある。

  • 非同期タスクの実行
  • コンテナクラスに対する終了通知と,タスク実行の継続が可能であるか返す機能
  • タスク内で処理したデータを保存しておき,コンテナ側に引き渡す機能

そして次に,上記の非同期タスクの入れ物(ランナー)となるクラスが来る。

package seqtest;

import seqtest.*;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;


// 複数の非同期タスクを逐次実行する。synchronizedとは違ったアプローチで。
class AsyncTasksRunner
{
    // 実行したい非同期タスク達
    private SequentialAsyncTask[] tasks;
    
    // 現在取り扱い中のタスクのインデックス
    private int executing_task_cursor = 0;
    
    // 全タスクから返されるデータ
    private HashMap<String, Object> data_from_all_tasks = new HashMap<String, Object>();
    
    
    // 初期化
    public AsyncTasksRunner( SequentialAsyncTask[] tasks )
    {
        this.tasks = tasks;
    }
    
    
    // 全タスクを実行開始
    public void begin()
    {
        log("begin()が呼ばれました。");
        
        if( tasks.length > 0 )
        {
            log("最初のタスクの実行を開始します。");
            executeCurrentTask();
        }
        
        log("begin()を終了します。");
    }
    
    
    // 現在のタスクを実行
    private void executeCurrentTask()
    {
        log("現在のタスクを実行します。インデックスは" + executing_task_cursor);
        getCurrentTask().kickByRunner( this );
    }
    
    
    // 現在取り組み中のタスクを返す
    private SequentialAsyncTask getCurrentTask()
    {
        return tasks[ executing_task_cursor ];
    }
    
    
    // 現在のタスクの実行完了時に呼ばれる
    public void onCurrentTaskFinished()
    {
        // 現行タスクの終了処理
        mergeDataFromTask( getCurrentTask() );
        
        if( mustMoveToNextTask() )
        {
            log("次のタスクへ移動する事を決定");
            
            // 次のタスクへ移動
            executeNextTask();
        }
        else
        {
            log("次のタスクへ移動しない事を決定");
        }
    }

    
    // 現在処理中のタスクが最後のタスクかどうか返す
    private boolean isProccessingLastTask()
    {
        return ( tasks.length == ( executing_task_cursor + 1 ) );
    }
    
    
    // タスクを継続可能かどうか返す
    private boolean mustMoveToNextTask()
    {
        // 最後のタスクでなく,現行タスクの結果がOKであれば。
        return (
            ( ! isProccessingLastTask() ) &&
            ( getCurrentTask().tasksContinuable() )
        );
    }
    
    
    // 1タスクの保持データを回収
    private void mergeDataFromTask( SequentialAsyncTask task )
    {
        // HashMapを取り出して,そのループの準備をする
        HashMap<String, Object> data_from_current_task = task.getStoredObjects();
        Set<String> keySet = data_from_current_task.keySet();
        Iterator<String> keyIterator = keySet.iterator();
        
        // 全キーについて,ランナー側に値を取り込む
        while( keyIterator.hasNext() ) 
        {
            // 1ペアを読み込み
            String key = (String)keyIterator.next();  
            Object value = data_from_current_task.get( key );
            
            // 1ペアを上書き
            data_from_all_tasks.put( key, value );
            log("ランナー側で," + key + "の値を上書きしました。");
        }
            // http://www.syboos.jp/java/doc/hashmap-usage-sample.html
    }
    
    
    // 次のタスクを実行
    private void executeNextTask()
    {
        executing_task_cursor ++;
        executeCurrentTask();
    }
    
    
    // タスクから回収した値をキーごとに返す
    public Object getDataByKey( String key )
    {
        return data_from_all_tasks.get( key );
    }
    
    
    // デバッグ用
    private void log( String s )
    {
        System.out.println( "[DEBUG] AsyncTasksRunner : " + s );
    }
    
}

このクラスの役目は,下記のようなもの。

  • 複数の非同期タスクをラップし
  • 最初から順番に実行し
  • それぞれのタスクから終了通知を受け取り次第,次のタスクの実行に着手する。
  • 全タスクの処理結果をハッシュマップ形式で保持する。

以上,ベンダー提供側と合わせて5つのjavaファイルをコンパイルする。

seqtestというフォルダ内にソースコードが存在するとして

D:\temp>javac seqtest/*.java

実行してみると・・・

D:\temp>java seqtest/Main

[DEBUG] AsyncTasksRunner : begin()が呼ばれました。
[DEBUG] AsyncTasksRunner : 最初のタスクの実行を開始します。
[DEBUG] AsyncTasksRunner : 現在のタスクを実行します。インデックスは0
[DEBUG] seqtest.Main$1 : 別スレッドでのタスク実行を開始しました。
[DEBUG] AsyncTasksRunner : begin()を終了します。
main()が終了。
[DEBUG] seqtest.Main$1 : main外のスレッドで,1から100までの合計値を計算します。
[DEBUG] seqtest.Main$1 : 計算終了。この値を別のタスクから参照可能になるよう保持。
[DEBUG] seqtest.Main$1 : タスク内で,sum1というキーの値をハッシュに登録しました。
[DEBUG] seqtest.Main$1 : 別スレッドでのタスク実行を終了しました。実行結果はtrue
[DEBUG] AsyncTasksRunner : ランナー側で,sum1の値を上書きしました。
[DEBUG] AsyncTasksRunner : 次のタスクへ移動する事を決定
[DEBUG] AsyncTasksRunner : 現在のタスクを実行します。インデックスは1
[DEBUG] seqtest.Main$2 : 別スレッドでのタスク実行を開始しました。
[DEBUG] seqtest.Main$2 : 前の非同期タスクの実行結果は5050
[DEBUG] seqtest.Main$2 : main外のスレッドで,101から200までの合計値を計算します。
[DEBUG] seqtest.Main$2 : 計算終了。この値を別のタスクから参照可能になるよう保持。
[DEBUG] seqtest.Main$2 : タスク内で,sum2というキーの値をハッシュに登録しました。
[DEBUG] seqtest.Main$2 : 別スレッドでのタスク実行を終了しました。実行結果はtrue
[DEBUG] AsyncTasksRunner : ランナー側で,sum2の値を上書きしました。
[DEBUG] AsyncTasksRunner : 次のタスクへ移動する事を決定
[DEBUG] AsyncTasksRunner : 現在のタスクを実行します。インデックスは2
[DEBUG] seqtest.Main$3 : 別スレッドでのタスク実行を開始しました。
[DEBUG] seqtest.Main$3 : 前の非同期タスクの実行結果は15050
[DEBUG] seqtest.Main$3 : 前の前の非同期タスクの実行結果は5050
[DEBUG] seqtest.Main$3 : 別スレッドでのタスク実行を終了しました。実行結果はtrue
[DEBUG] AsyncTasksRunner : 次のタスクへ移動しない事を決定

D:\temp>

各タスクがちゃんと実行されている。

タスク間での情報の連携もできている。



いわば,ワーカスレッドのキューを作る事によって,

トップの処理記述部分を,極めて簡潔なコードで済ませたわけだ。

ベンダ側から提供される非同期タスクの処理方式に応じて,コードの中身は具体的に作りなおせばよい。



このアプローチは,あらゆる局面で応用できる。

  • まず,タスクを個別に扱うのをやめる。(コールバックの入れ子地獄になるので)
  • かわりに,ランナーに複数のタスクをセットして,ランナーを走らせる。
  • ランナーは,複数のタスクを順番に実行させ,データの連携も請け負う。

こうすることで,並列プログラミングの世界を

あたかも逐次プログラミングの世界に変換してしまう。


そうすれば,コードは仕様をより明確に表現する事ができるようになり,

スレッド間連携の手間も減るだろう。


補足

下記エントリの「結び」を参照。

Androidアプリで,HTTP通信のPOSTリクエストをする汎用クラス (文字化け無し+非同期タスク)
http://language-and-engineering.hatenablog.jp/entry/20111121/p1

ユーザビリティを向上させ,ANRの発生を極力防ぐためには,どうしても非同期タスクに頻繁に頼ることになる。
確かにそれはセオリーだ。


しかし,非同期タスクを多用すると,ソースコードから処理フローが追いづらくなる。
コールバックベースのスタイルでコーディングするために,リスナとかハンドラを,バラバラに別個のオブジェクトとして細分化して定義しなければならない。
そうすると,処理の流れに沿ったコーディングができない。

「ソースコードが設計書になってくれない」のだ。

・・・
Javaの言語仕様としてクロージャが欲しい,という事に尽きる。


・・・
Rubyでは「考えた通りの順番でコーディングすればその通り動く」。
jQueryのように,本来はコールバックの連鎖であるはずの一群の処理を,美しいメソッドチェインで記述する事によって,処理仕様を明示的に示す。という事も,動的言語ならやりやすい。

補足2

なんと,本エントリに対して,あの id:ryoasai 氏がブックマークコメントを述べて下さった。

id:ryoasai 氏は,はてなダイアラーの中で言うと,Javaとソフトウェア開発に関するアルファブロガーだ。

私は,彼のブログは欠かさず常にチェックしている。
私がとても尊敬するJavaエンジニアの一人。

記事の質がきわめて高く,毎回新記事が出るたびに「その通りだよな」と共感を覚える事も多い。*2

達人プログラマーを目指して
http://d.hatena.ne.jp/ryoasai/


ただ残念ながら,氏より本記事に対して頂いたコメントは,余りにセオリー通りの内容だったので,少々拍子抜けしてしまったのが実際のところだ。


コメントを下記に引用させていただく。

ryoasai [java, 並行処理, 残念]


synchronizedを使わない方法とありますが、AsyncTasksRunnerは同期化してスレッドセーフでなくてはいけません。また、実践ではjava.util.concurrentを使うべきで、TaskなどはRunnableを使った方がよいでしょう。 2012/02/08

ryoasai のブックマーク
http://b.hatena.ne.jp/ryoasai/2012020...

しかしこれは重要な定石なので,補足として掲載する価値は高いと思う。

  • AsyncTasksRunnerは同期化してスレッドセーフでなくてはいけません。
    • →スレッドセーフな設計は基本中の基本となるので,これは真実だ。本エントリを見た人が「synchronizedは使わなくていいんだ!」とか,「スレッドセーフは無視していいんだ!」などど誤った結論を導出したら,大変な害になってしまう。スレッドセーフの重要性について,一言ぐらい言及しておくのがスジだったと言えるだろう。指摘に感謝である。
    • とはいえ,タイトルや全体のメッセージからわかる通り,本エントリの要旨は「複数の非同期タスクを逐次化する」こと,つまり複数のスレッドを同時並行して走らせるのではなく,管理オブジェクトが一度に一つだけの非同期タスクをさばくような仕組みを示す事にある。
    • 実際の業務では,当然のことだが,管理オブジェクト側に各種の例外処理を埋め込む必要が生じる。
      • 例えば,一つの非同期タスクは,管理オブジェクト側に「終了しました」と通知したにも関わらず,実はまだ動き続けているかもしれない。だから,管理オブジェクト側をスレッドセーフにする必要があるという指摘ももっともだ。だがしかし,そういったケースを除外して,正常系となるシンプルなケースを冒頭のUMLで最初に示し,前提としたうえで議論していることに注目を。異常系のためにサンプルコードを膨らませてしまうと,サンプルコードとしての役目を果たさなくなってしまう。伝えたいアイデアをピンポイントで示せなくなってしまうからだ。
  • 「実践ではjava.util.concurrentを使うべきで、TaskなどはRunnableを使った方がよいでしょう。」
    • "standardなJava" の定石なので,こういった補足を掲載すべきだったかもしれない。この点も,やはりツッコミに感謝である。
    • とはいえ,何かの "standardではないJava" を使っている場合,どうなるのであろうか。


ryoasai氏は,非常にお忙しい方でもある。

なので,本エントリが“どういう「伏線」を持って執筆された記事なのか”

などと,じっくり分析する暇など,当然ないことだろう。

本エントリには,ちらっと目を通して頂いただけでも特権だ。身に余るレビューだと思う。

twitter上でも感謝の言葉を申し上げた。
https://twitter.com/#!/lang_and_engine/status/167028514304950273


この記事は,ピュアJavaという化けの皮を意図的にかぶらせて公開せざるを得なかったわけだが,それはあくまで化けの皮に過ぎないので,矛盾っぽいものが見つかる事もあるだろう。

※参考:

本ブログを長年購読されている方々がもしいるとすれば,しばしば特定のエントリが謎めいた仕方で終わり,数カ月以内に「実はこんな仕方で応用するための材料でした」という形で再度取り上げていることにお気づきだろう。
(いわば,エントリの継承)


あるいは明示的に取り上げないように見える場合であっても,ブログ外の現実世界において,私は応用しているのである。
つまり,そのための下地がこのブログ,という位置づけになる。


現実世界の成果を何もかも逐一,ネット上に晒すような事は私はしない。
守秘義務違反だし,そうすると肝心な私のビジネスの種が奪われる結果になり,自分で自分の首を絞める結果になるから。それに,そのような時間もない。


もちろん,そういった危険が存在しない範囲で,ブログに情報を公開している。
私が所属するチームメンバの内部でシェアしやすいからだ。


ヒントを言おうか。

私は常に,ビジネス的に一番大事な情報は,下記のようなフレーズを使って隠ぺいする。
(いわば,エントリのカプセル化)

  • 「補足」「参考」「参照」
  • 「ちなみに」「なお」
  • 「が,あればいいのだが。」
  • 「は,慎重に考える必要があると言えるだろう。」
  • 「からは,何らかの示唆を得られるかもしれない。」
  • 「も興味深い。」「を考えてみるのもまた一興。」
  • 「という場合にどうなるのか,については,別途吟味のこと。」
  • 「なのか,という点が問われることになるだろう。」
  • あるいは,URLから文章を引用する際に,的を絞って引用文を抽出する事により。
  • または,話題をそらす事により。
  • 今回のエントリのように,あえてEclipseを使わず,なぜか手動コンパイルする事により。

こういった隠ぺいを行ないつつも,筋道だった論理的な文章をしたためようとすると,どうなるであろうか。

私がここで執筆する日本語の文章中では,逆接の接続詞が多くなる。ここぞという箇所では,不自然な疑問文として筆記せざるを得ない場合もある。


下記エントリも参照のこと。


このブログの存在目的
http://language-and-engineering.hatenablog.jp/entry/20080101/p1


ところで,「エントリのポリモーフィズム」って何だろうなあ。解釈の仕方がたくさんあるように思う。


とは言っても,別に本エントリがベストの設計だと主張しているわけでもなし,

非同期タスクをさばくためのDSLのアイデアとして,本記事を誰かが読んでくれて,

その方がよりいっそう優れたコードをネット上で公開してくれたら,それは元記事の執筆者にとっても,全Web利用者にとってもありがたく望ましいのだ。

本ブログについてそういったケースが今まで多くあったので,今回のエントリも,誰かのDSL欲を刺激する結果になれば嬉しく思う。



では,ブコメの形式で貴重なレビューコメントを下さったid:ryoasai氏(やid:j5ik2o氏)への感謝を重ねて申し上げ,本エントリの結びとしたい。

達人プログラマーを目指して」をまだ購読していない人は,この機会に,ぜひ隅から隅まで熟読しましょう。

言うまでもなく,「達人プログラマー」もね。






 

*1:JavaScriptを例に挙げたが,Javaと違ってJavaScriptがシングルスレッドである,という事実はお忘れなく。 http://language-and-engineering.hatenablog.jp/entry/20090614/p1

*2:時間差で,id:ryoasai氏だけでなく,id:j5ik2o氏からのコメントも頂いた。私は,かとう氏の発信する価値ある情報も欠かさずチェックしている。残念ながら紙面が不足したため書ききれないのだが,id:j5ik2o氏にも同様の感謝のコメントを申し上げたい。