はじめに

複数のAjaxを実行すると、各処理の終了を待たずして非同期でAjaxが動くため、同時に処理をしてしまう。
一つずつ処理をするには、async : falseを設定して同期処理に変更したり、Ajax成功時のコールバック部分でAjaxを再帰呼び出ししたりなどの方法がある。

大量のAjax処理を全て同時に実行するのは避けつつも、効率化のため一つずつではなくある程度の数を並列化したいとなった場合に、どのように実装するかを書く。

例題

今回の処理を実装するにあたり、例題を設定する。

要件

  • ファイル名と処理状態の二つの列をもつ一覧がある。
  • 処理状態の候補値は次の通り。

    • 準備中
    • 処理中
    • 成功
    • 失敗
  • 実行ボタンを押下すると、一覧を上から順に次のように処理する。

    • 処理状態が「準備中」でない場合、次の行へスキップする。
    • 処理状態を「処理中」へ変更する。
    • ファイル名をAjaxでPOSTし、サーバ側である程度時間のかかる処理を行う。
    • Ajaxのレスポンスでjson.result == 'OK'と返ってくれば処理状態を「成功」に、返らなければ「失敗」に変更する。
  • 一覧のすべての処理が完了したら、alert('完了');を実行する。

  • 処理の多重度を指定できるようにして、処理速度を高速化する。

実装

一覧部分の実装

Ajaxの多重度を指定した並列実行という本筋とは関係ないが、まずはじめに一覧部分がどのようになっているのかを示す。
一覧はjqGridで実装されているものとする。

$(function(){
    $('#grid1').jqGrid({
        datatype: 'json',
        mtype: 'GET',
        url: '/loadFiles',
        rowNum: '',
        shrinkToFit:false,
        height: 160,
        width: 788,
        hidegrid : false,
        viewrecords: false,
        altRows : true,
        altclass : 'jqgridAltRows',
        colNames:[ // ヘッダ行
                    'ファイル名',
                    '処理状態'
                ],
        colModel :[ // 明細行
                    {name:'file', resizable:true, sortable:false, width:520, align:'left'},
                    {name:'status', resizable:true, sortable:false, width:185, align:'left'}
                 ],
        gridComplete: function() {
                },
    });
});

多重度の制御機能

多重度の制御機能を実現するため、変数の状態を記憶し、また変数をprivateにできるクロージャを使ったクラスベースオブジェクト指向で実装する。

ファイル一覧が処理対象なので、ファイル一覧の状態を管理し、またファイル一覧に対しての処理を行うメソッドをもつクロージャが必要になる。
FileManager(concurrentLimit)と定義し、引数concurrentLimitで、多重度の限界値を指定できるようにする。

function FileManager(concurrentLimit) {
    var fileList = [];
    var statusList = [];
    var ids = $('#grid1').jqGrid('getDataIDs');
    for (var i = 0; i < ids.length; i++) {
        var rowData = $('#grid1').jqGrid('getRowData', ids[i]);
        fileList.push(rowData['file']);
        statusList.push(rowData['status']);
    }

    var registerAjax = function(idx) {
        var file = fileList[idx];
        $.ajax({
            type : 'post',
            dataType : 'json',
            url : '/register',
            cache : false,
            data : {
                "file" : file
            },
            success : function(json){
                if (json.result == 'OK') {
                    changeStatus(idx, '成功');
                } else {
                    changeStatus(idx, '失敗');
                }
            },
            error : function(jqXHR ,test,error){
                changeStatus(idx, '失敗');
            }
        });
    };

    var changeStatus = function(idx, txt) {
        statusList[idx] = txt;
        $('#grid1').jqGrid('setCell', ids[idx], 'status', txt);
    };

    return {
        // return true when limited.
        limited : function() {
            var count = 0;
            for (var i = 0; i < statusList.length; i++) {
                if (statusList[i] == '処理中') {
                    count++;
                    if (count >= concurrentLimit) {
                        return true;
                    }
                }
            }
            return false;
        },

        // return idx. If nothing, return -1.
        get : function() {
            var idx = $.inArray('準備中', statusList);
            if (idx == -1) {
                return -1;
            }
            changeStatus(idx, '処理中');
            return idx;
        },

        execute : function(idx) {
            registerAjax(idx);
        },

        // return true when finished.
        finished : function() {
            for (var i = 0; i < statusList.length; i++) {
                if (statusList[i] == '準備中' || statusList[i] == '処理中') {
                    return false;
                }
            }
            return true;
        },
    };
}

各メソッドの説明

メソッド 説明
limited 一覧の中で「処理中」の個数を数え、多重度の限界値に達しているかを判定する。
get 一覧から一番最初に出てくる「準備中」の行のIndexを返す。「処理中」に変更する。
execute Ajaxを実行する。成否によって「成功」、「失敗」に変更する。
finished 一覧の処理状態を全て確認し、Ajaxがすべて終了したかを判定する。

FileManagerにおけるエッセンスは多重度を判定するlimitedだ。

ループ & スリープによる呼び出し

FileManagerをループとスリープを使いながら呼び出す関数を作る。実行ボタンはクリック時、この関数を呼び出す。

まずはじめに、多重度つきの並列実行のエッセンスに着目するため、今回の要件の内、「一覧のすべての処理が完了したら、alert('完了');を実行する。」を抜かして実装する。

var manager;
function register() {
    var sleep = 1000;
    manager = FileManager(3);
    var loop = setInterval(function() {
        if (!manager.limited()) {
            // 処理対象取得
            var idx = manager.get();
            // 処理対象がなくなれば
            if (idx == -1) {
                clearInterval(loop);
                return;
            }

            // 登録処理実行
            manager.execute(idx);
        }
    }, sleep);
}

JavaScriptにはsleep関数がないので、setInterval, clearIntervalを使ってループとスリープ、そしてbreakを実現している。
ループしながら並列数の限界値を超えていないかどうかlimitedで確認し、超えていなければget, executeと処理を行っていく。executeは非同期処理なのでレスポンスが返る前にメソッドが終了する。これでスリープする時間分のタイムラグはあるものの、並列実行ができる。

次に、抜かした要件部分も実装を行う。「ajax処理終了判定」をclearInterval(loop);後に実行する。いつレスポンスが返ってくるかわからないので、ループとスリープを使って判定をしなければいけない。構造は外側を囲っているループと同じ。

var manager;
function register() {
    var sleep = 1000;
    manager = FileManager(3);
    var loop = setInterval(function() {
        if (!manager.limited()) {
            // 処理対象取得
            var idx = manager.get();
            // 処理対象がなくなれば
            if (idx == -1) {
                clearInterval(loop);

                // ajax処理終了判定
                var finishLoop = setInterval(function() {
                    if (manager.finished()) {
                        clearInterval(finishLoop);
                        alert('完了');
                    }
                }, sleep);

                return;
            }

            // 登録処理実行
            manager.execute(idx);
        }
    }, sleep);
}

以上で多重度を指定してAjaxを並列実行する処理は完成である。

理解補助のためsleepメソッドのあるJavaで書くと、次のような処理になる。

@SneakyThrows
void register() {
    int sleep = 1000;
    manager = new FileManager(3);

    while (true) {
        if (!manager.limited()) {
            // 処理対象取得
            int idx = manager.get();
            // 処理対象がなくなれば
            if (idx == -1) {
                // ajax処理終了判定
                while (true) {
                    if (manager.finished()) {
                        System.out.println("完了");
                        return;
                    }
                    Thread.sleep(sleep);
                }
            }

            manager.execute(idx);
        }
        Thread.sleep(sleep);
    }
}