実際(JS,Clojure,python)の非同期処理

ウェブ技術 デジタルトランスフォーメーション技術 人工知能技術 自然言語処理技術 セマンティックウェブ技術 深層学習技術 オンライン学習&強化学習技術 チャットボットと質疑応答技術 ユーザーインターフェース技術 知識情報処理技術 推論技術  Clojure Javascript  プログラミング

前回、並行/並列と非同期処理の概要について述べた。今回はいくつかの言語で実際に確認する。

まずjavascriptのケースについて@kiyodoriさんの。「非同期処理ってどういうこと?JavaScriptで一から学ぶ」に良い例があったので転記する。まず通常動作ののjavascript、以下のファイルをsync_sleep.jsとして準備する。

console.log("start");

function sleep(milliSeconds) {
  var startTime = new Date().getTime();
  while (new Date().getTime() < startTime + milliSeconds);

  console.log("sleepが完了しました。");
}

sleep(5000); // 実行するのに5秒かかる

console.log("end");

nodeを使って上記のファイルを実行すると以下のようになる。

$ node sync_sleep.js
start
sleepが完了しました。
end

これに対して以下のようなファイルをnumber.txtとして準備し

1
2

以下のようなjavascriptファイルを作成しNodeで実行する。

console.log("start");

var fs = require('fs');
fs.readFile("number.txt", "utf-8", function(err, data){
  if (err) throw err;
  console.log("ファイルの読み取り準備ができました。");
  console.log(data);
});

console.log("end");

実行結果は以下となる。

$ node async_readfile.js
start
end
ファイルの読み取り準備ができました。
1
2

前回のものと異なり、コード内では先に記述されているファイルの読み取りが、後のコードで記述されているend表示よりも後ろで実行されている。ファイルの読み取り関数fs.readFileは非同期関数として予め定義されておりそれが呼び出されると一旦止まり、後ろの部分を実行する為上記の出力になる。このようにjavascriptはwebブラウザでの動作というIOに関連した言語として生まれた為「そのように動作する」関数が最初から多数実装されている。

上記のようなデフォルトの関数以外では、これまでも何度も登場したコールバック関数を利用する。以下にサンプルを示す。

function sleep(callback) {
  setTimeout(function() {
    callback(null, '現在時刻:' + new Date());
  }, 1000);
}

sleep(function(err, res) {
  console.log(res);
  sleep(function(err, res) {
    console.log(res);
    sleep(function(err, res) {
      console.log(res);
    });
  });
});

このように引数にコールバック関数を用いて多段の非同期処理を実実行してしまうとコールバック地獄とよばれる複雑な処理になる。

これに対して他の言語では、それぞれの特徴を持った「非同期処理」に対応したライブラリが用意されており、それらと通常の関数を組み合わせて利用する形となる。

たとえばClojureではこれをcore.asyncというライブラリで実現する。これは並列処理がが得意なGo-langのコンセプトを用いたgoマクロによってgo-blockを作り、そのブロック内のみで非同期動作を実現するという形を取る。またブロック間でのデータをやり取りするデータとしてチャネル(channel)を使う。

goマクロはステートマシンを作り、チャネルへの入力があるたびにマシンが1回転します。この一回転時に、チャネルを待ち受けていたgoブロックにスレッドが割り当てられ、次のチャネル入出力までCPUを使って処理が動き、チャネルの入出力でまた別のgoブロックに処理が映り、という形で、限られたCPU上で、スレッドを山ほど起動することもなく、効率よく動作する。

ここでClojureでのコード例を示す。

(import '[java.util Date])
(require '[clojure.core.async :refer [chan go-loop >! <! timeout] :as async])
(def ch (chan)) ; チャネルを作る

;; 書き込み非同期ブロック
(go-loop []
 (when (>! ch (Date.)) ; チャネルに書く
   (<! (timeout 2000)) ; 2秒待つ
   (recur)))

;; 読み込み非同期ブロック
(go-loop []
 (when-let [date (<! ch)] ; チャネルを読む
   (println "now:" date)
   (recur)))  

コードとしては非常にシンプルでチャネルを定義して、チャネルへの書き込みブロックと読み込みブロックをそれぞれ定義し、いずれのブロックもチャネルが閉じられるまでループし続ける。このようなブロック必要なだけ書いて並べていけば良い構造になっているので 前述のようなJavascriptでのコールバック地獄に落ちない。

比較としてpythonでの非同期処理のコードを示す。

mport asyncio

Seconds = [
    ("first", 5),
    ("second", 0),
    ("third", 3)
]

async def sleeping(order, seconds, hook=None):
    await asyncio.sleep(seconds)
    if hook:
        hook(order)
    return order

async def basic_async():
    # the order of result is nonsequential (not depends on order, even sleeping time)
    for s in Seconds:
        r = await sleeping(*s
        print("{0} is finished.".format(r))
    return True

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(basic_async())

ライブラリとしてはasyncioを使い、loop = asyncio.get_event_loop()で生成されるイベントループのなかでawait部で重い処理を規定して、そこに到達すると「イベントループ内の他の処理」を開始する事が特徴となる。

コメント

  1. […] 非同期処理 各種言語の比較、core.asyncの活用 […]

  2. […] 非同期処理 各種言語の比較 […]

タイトルとURLをコピーしました