caolan/asyncを使ったNode.jsのフロー制御
JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース)の16日目です。
@koichikさんの13日目の記事にも書かれているように、Node.jsにて非同期処理を管理する方法には以下の2種類があります
- 非同期処理の呼び出し時にコールバック関数を与える。処理が終わったらエラーの有無と、結果が通知される。
- EventEmitterに対してon/onceでEventListenerをセットして非同期処理を実行する。非同期処理の実行によって発生したイベントやエラーはリスナーに通知される。
後者の説明はkoichikさんの記事にお任せして、この記事は前者を扱うライブラリcaolan/asyncについてみていきたいと思います。
caolan/asyncって?
https://github.com/caolan/async で公開されているフロー制御ライブラリです。
自分はNode.jsで使っていますが、ブラウザ上で動くJavaScriptを書く場合にも使用できるようです。
https://github.com/joyent/node/wiki/modules#wiki-async-flowに記載されているNode.js用フロー制御モジュールは既に50を超えていますが、試してみた中では一番使いやすいモジュールでした。
# さすがに50すべては試せていないです。githubのwatcher数1000オーバー、folk数62ということで少なくともフロー制御モジュールの最大手ではあるはず。
asyncが実現してくれるフロー制御の一部はまさに「非同期プログラミングの改善」のエッセンスの内容なので、先に読んでおくと理解が深まるかと思います。
何をしてくれる?
asyncがやってくれる処理には様々なものがありますが、まずはasync#seriesを例にして説明していきたいと思います。
hogeディレクトリを掘って、piyoディレクトリを掘って、その下にデータを書きだすコードをコールバックを使って書くと以下のようになります。
(ディレクトリ堀は再帰的にやってくれるモジュールがありますが見なかった方向で)
var fs = require('fs'); fs.mkdir('./hoge', function(err) { if(err) { // エラー処理 return; } fs.mkdir('./hoge/piyo', function(err) { if(err) { // エラー処理 return; } fs.writeFile('./hoge/piyo/neko', 'Hello Node.js', function(err) { if(err) { // エラー処理 return; } console.log('success'); }); }); });
この例のように、非同期処理の終了を待ってから次の処理をさせる場合、ネストが深くなる、エラー処理があちこちに分散するといった問題点があります。
では、このコードをasyncを使った形で書き直してみましょう。
var fs = require('fs'); var async = require('async'); var tasks = []; tasks.push(function(next) { fs.mkdir('./hoge', next); }); tasks.push(function(next) { fs.mkdir('./hoge/piyo', next); }); tasks.push(function(next) { fs.writeFile('./hoge/piyo/neko', 'Hello Node.js', next); }); async.series(tasks, function(err, results) { if(err) { // エラー処理 return; } console.log('success'); console.log(results); // [undefined, undefined, undefined] });
はい、エラー処理が一カ所にまとまりました。
ネストが深くなることもなく、処理もtasksにまとまっていて良い感じです。
async#seriesは第一引数で与えられたtasksの処理を順番に実行してくれるメソッドです。
tasksに格納された各処理は、自分の処理が終わった時点でnext()を呼び出し、「エラーの有無」と「自分の処理結果」を通知します。
エラーが発生しなかった場合には自動的に次の処理が呼び出され、 エラーが発生した場合には、それより先の処理は実行されずにasync.seriesの第二引数に渡された関数が呼び出されます。
errには発生したエラー(エラーなしの場合はnull)が入り、resultsには各処理の処理結果がまとめて入ります。
今回使ったfs#mkdirやfs#writeFileはエラーの有無のみを通知する為、resultsに関しては次項の例を見てもらった方が分かりやすいかと思います。*1
async#series, async#parallel
asyncモジュールにはseries以外にも様々なメソッドが用意されています。
ここからは、それらの処理の流れを図で表しながら簡単に説明していきたいと思います。
まずはasync#seriesです。
先ほど説明した通り、一つの処理が終わってから次の処理が行われ、それぞれの処理結果がendに集約されています。
凡例:
・青い四角:処理
・赤い四角:データ
・青線:処理の流れ
・赤線:データの流れ
・finish:async#****を呼び出すときに与えられるcallback関数
(エラー発生時か、全部の処理が終わった後に呼ばれる関数)
# 図はCacooにて作成。Excelは図を描くツールではないのです。
つづいて、async#parallel。こちらは全部の処理が同時に実行され、すべてが終わった時点でendが呼ばれます。
この場合もseriesと同じく、それぞれの処理結果はendに集約されています。
async#seriesとasync#parallelの違いを見るために「delayミリ秒待ってからtextの内容を出力し、処理結果としてresultを返す関数」を作って試してみます。
function delayedPrint(delay, text, result) { return function(next) { setTimeout(function() { util.log(text); next(null, result); }, delay); } } var tasks = []; tasks.push(delayedPrint(3000, 'test1', 'result1')); tasks.push(delayedPrint(2000, 'test2', 'result2')); tasks.push(delayedPrint(1000, 'test3', 'result3'));
async#seriesの場合
util.log('start'); async.series(tasks, function(err, results) { util.log('true end'); console.log(results); }); util.log('end? ... NO. while processing.');
実行結果:
16 Dec 01:39:13 - start 16 Dec 01:39:13 - end? ... NO. while processing. 16 Dec 01:39:16 - test1 16 Dec 01:39:18 - test2 16 Dec 01:39:19 - test3 16 Dec 01:39:19 - true end [ 'result1', 'result2', 'result3' ]
async#parallelの場合
util.log('start'); async.series(tasks, function(err, results) { util.log('true end'); console.log(results); }); util.log('end? ... NO. while processing.');
実行結果:
16 Dec 01:40:15 - start 16 Dec 01:40:15 - end? ... NO. while processing. 16 Dec 01:40:16 - test3 16 Dec 01:40:17 - test2 16 Dec 01:40:18 - test1 16 Dec 01:40:18 - true end [ 'result1', 'result2', 'result3' ]
これらの結果から以下のことが分かります。
- end?の時点ではまだ処理が終わっていない。全部の処理が終了したのはtrue endの時点。
- startからendまでの時間がseriesでは直列実行の為、3 + 2 + 1 = 6秒
- startからendまでの時間がparallelでは並列実行の為で、max(3, 2, 1) = 3秒
- parallelの例を見るとわかるようにresultsに入ってくるのは処理の完了順ではなくtasksの順
async#waterfall
async#waterfallを図に表すと以下のようになります。
処理の流れ自体はseriesと同等ですが、seriesと違い、処理結果が次の処理に与えられます。
便利なメソッドであることは確かなのですが、このメソッドに関しては気になっていることがある為、後日改めてエントリを起こそうと思います。
async#forEach, async#forEachSeries
前項で紹介したseries, parallel, waterfallは複数の処理の流れを制御する形でしたが、配列の各要素に対して同じ処理を実行することも良くあります。
同期処理であればArray#forEachを使えばよいのですが、その中に非同期処理が含まれている場合、Array#forEachから処理が返った時点では配列の各要素に対する処理が終わっていません。
asyncではこういった場合に使えるメソッドとしてforEach, forEachSeriesというメソッドを用意しています。
名前からもわかるようにforEachは並列処理を行い、forEachSeriesは直列に処理を行います。
async#map, async#mapSeries
forEachやforEachSeriesは配列の各要素に対して処理を行いますが、処理結果を返す機能は持っていません。
配列の要素を変換したい場合などにはArray#mapと同じく、async#mapを用います。
これもforEachと同じくasync#mapとasync#mapSeriesの二つが用意されているため、変換ロジックの重さに応じて好きな方を選ぶことができます。
ほかの関数
今回は紹介しきれませんでしたが、caolan/asyncには他にも以下のようなメソッドを提供しています。
- makeファイルのように各タスクに前提条件を指定すると、自動的に順番を組み立ててくれるauto
- forEach/mapのようにArrayが提供していた処理をやってくれるfilter, filterSeries, reject, rejectSeries, reduce, reduceRight, concat, concatSeries, some, any, every, all
- 条件を満たす間 or 満たすまで処理を続けるwhilst, until
- 並列で実行する最大数を指定して大量のタスクを処理させられるqueue
- 重い処理の処理結果を自動的にキャッシュして二回目以降の呼び出しを軽くするmemoize
これらのメソッドとEventEmitterをうまく使うことで処理の流れを整理し、処理の流れが分かりやすいコードを目指しましょう!
明日は@n_matsuiさんです。
*1:例選びに失敗した感がひしひしと
MacBookAirでWindowsを使う場合のキー設定
MacBookAirにBootCampでWindowsを入れて使っていますが109キーボードとは形状がことなる為、特殊キーの入力方法がややこしかったり、違和感があったりします。
自分が設定した内容をまとめてみました。
特殊キーの入力方法
まずは押し方次第で入力できる特殊キーの一覧。
Pause, Insert, PrintScreen, ScrollLockあたりは一風変わったキー入力ですね。
Windows環境のキー(仮想キーコード) | MBAのキー入力 |
---|---|
Backspace (0x08) | delete |
Pause (0x13) | fn + esc |
無変換 (0x1D) | 英数 |
ひらがな | かな |
Insert (0x2D) | fn + enter |
Delete (0x2E) | fn + delete |
左Win (0x5B) | 左command |
右Win (0x5C) | 右command |
IME ON/OFF | caps |
PageUp (0x21) | fn + ↑ |
PageDown (0x22) | fn + ↓ |
Home (0x24) | fn + ← |
End (0x23) | fn + → |
PrintScreen (0x2C) | fn + 左shift + F11 |
ScrollLock (0x91) | fn + 左shift + F12 |
Ctrl + Alt + Delete | fn + control + alt + delete |
キーレイアウトの変更
Windows環境の場合、以下のレジストリを変更することである程度キーレイアウトを変更することができます。
\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout\Scancode Map
ChangeKeyというフリーソフトでも変更可能です。このアプリが中でやっていることはレジストリの変更なので「管理者として実行」で起動する必要あり。
自分の場合、普段使っているのが91キーボードなので、こんな風にレイアウトしてみました。
- 「Altキー」 → 「左Winキー」(交換)
- 「左Winキー」 → 「Altキー」(交換)
- 「右Winキー」 → 「アプリケーションキー」
- アプリケーションキー(右クリック同等)を入力する方法がShift + F10*1しかなかったので、使わない右Winキーを潰して割り当て
Googleで一文字検索してみた。(2011年版)
ふと思いついたので2008年にやった一文字検索を再度動かしてみた。
結果
雑感
- 3年前のスクリプトがそのままでも動くGoogleすごい。class="l"とか1文字なので心配していたけど大丈夫だった
- cinwz辺りを見ると単一文字の商品名でも一位をとれるみたい。SEOの成果?
- hはやっぱりジャンル的には変わらないらしい
- sは引き続き静岡
- yがYahooオークションからYahooJapanへ
- 人生オワタの一位オワタ\(^o^)/
- これのURL、http://ja.wikipedia.org/wiki/\(^o^)/だったのな。
ソース
require 'rubygems' require 'hpricot' require 'open-uri' require 'jcode' query = "abcdefghijklmnopqrstuvwxyz"; query.each_char do |letter| url = "http://www.google.co.jp/search?q=#{letter}&lr=lang_ja" doc = Hpricot(open(url).read) result = (doc./"//h3.r/a.l")[0] puts "<tr><td>#{letter}</td><td><a href=\"#{result[:href]}\">#{result.inner_text}</a></td></tr>" end
素のままでも動いたけど、Youtubeの別枠扱いをなくすためにXPathを変更
今ならhpricotではなくNokogiriとかかなぁ。
Ruby製バウリンガル
エイプリルフールも終わったのでわんわんをデコードするRuby製バウリンガルを載せてみる。
$KCODE = 'SJIS' require 'zlib' decode_table = { 'わん' => 0, 'きゃん' => 1, 'わおん' => 2, 'わーん' => 3, 'ばう' => 4, 'きゃうん' => 5, 'わう' => 6, 'きゅーん' => 7 } encoded_text = ARGF.readlines.join.gsub(/[\r\n。!]/, "") regexp = Regexp.new(decode_table.keys.join("|")) octets = encoded_text.scan(regexp).map {|octet| decode_table[octet]} compressed_text = octets.each_slice(3).map {|octet| octet.join.to_i(8) }.pack("C*") $>.write(Zlib::Inflate.inflate(compressed_text))
東京国際アニメフェアのチラシをCanvas使って編集してみる
Twitterを見ていたらRTでこんなのが流れてきたわけですよ。
だれか探偵もの漫画の人物紹介ページみたいに出店しなくなったアニメをシルエット表示に変えてくれないかな。 URL
2010-12-10 18:49:38 via web
最近Canvas使ってないなー。と思ったのでカリカリとBookmarklet書いてみました。
使い方
Firefox3.6.12とChrome8.0.552.215で動作確認。
1. Hatena::Letを開きます
2. 「bookmarklet:東京国際アニメフェアの開催告知チラシから参加拒...」をドラッグしてお気に入りに放り込みます。
3. 公式チラシ | 東京国際アニメフェア2011を開きます
4. 2番で追加したお気に入りを選択するとこんな感じになります。
しかし、最近の作品がわからない・・・
2010/12/11 01:58追記:
http://twitpic.com/3elscd にすごく分かりやすい画像があったので使わせてもらいました。
金星探査機あかつきの現状まとめ
12月7日22時の記者会見のまとめが主、手元にまとめたテキストをコピペ
時間のズレとかあるかもー 08時49分 軌道制御エンジン噴射開始 08時50分43秒 通信断絶開始(予定通り) 09時01分00秒 軌道制御エンジン噴射終了 09時12分03秒 通信再開予定だったが通信再開せず 10時03分 探査機側で通信アンテナをLGAに自動切り替え 10時28分 衛星のLGAからの通信が受信される MGAに切り替えるよう、衛星にコマンドを送信 コマンドは送信でき、MGAに切り替えが出来た 10時59分 MGA通信確立できず 11時29分 通信確立できなかったので、衛星側でLGAに自動切換 14時00分 スペインマドリッド局にて補足、LGAから受信、強さ変化確認 0.1rpm(10分で1周の周期)で回転していることが確認される セーフホールドモードと考えられるがこの時点では未確定 MGAに切り替えるように地上から地上からのコマンド 10分に1回、40秒受信可能 17時00分 回転停止コマンドを送信したが停止せず、セーフホールドモード継続 この際止まらなかったのはコマンドが衛星に届いていないと推定されている 衛星の軌道が本来と違う為に、アンテナがズレていた 21時00分 スペインマドリッド、アンテナ方向予測して修正 MGAでも通信できたが安定しない為、LGAで通信してテレメトリデータを受信中 現状: セーフホールドモードに入っているのは確定 LGAにてテレメトリの受信(22時〜24時ぐらい) 軌道は不明 トラブルなければ明日の昼までに採れるはずの情報 HouseKeeping情報は分かる 軌道決定もできる・・と良いな 参考: 地上の光回線 100000000 bps(100Mbps) HGA 256000 bps(256kbps)? MGA 512 bps LGA 8 bps
Amazon EC2/S3/etc.の無料サービスに登録してみた
Amazonが11月1日から機能限定での無料サービスを始めたので、遅ればせながら試しに登録してみた。
AWSアカウントを登録
1. 公式ページ(http://aws.amazon.com/free/)のSign Up Nowボタンから登録ページへ
2. メールアドレス、氏名、パスワード、住所、国、電話番号辺りを登録
注意:郵便番号にハイフンが無い場合、Invalid Addressといわれて受け付けられない
3. 登録完了
EC2の利用申請
EC2のページ(http://aws.amazon.com/jp/ec2/)からAmazon EC2の利用を申し込む
Free tier for new AWS customersとして条件が書かれていたり
アジア地域の場合はMicroInstanceで0.025$/hour。
普通に使った場合はこんな感じ
- Amazon EC2 Linux/Unix Micro Instance
- 0.025($) * 80(円/$) * 24(時間) * 31(日) = 1488円ぐらい
- Elastic Load Balancing
- 0.028($) * 80(円/$) * 24(時間) * 31(日) = 1666円ぐらい
- 0.008($) * 80(円/$) * 15(GB) = 9.6円ぐらい
- Amazon Elastic Block Storage(EBS)
- 0.11($) * 80(円/$) * 10(GB) = 88円ぐらい
- 0.11($) * 80(円/$) * 1(million IOs) = 8.8円ぐらい
- 0.15($) * 80(円/$) * 1(GBSnapshot) = 12円ぐらい
- 0.01($) * 80(円/$) * 1(x1000 PUT)= 0.8円ぐらい
- 0.01($) * 80(円/$) * 1(x10000 GET)= 0.8円ぐらい
後半すごく細かいが、合計すると3274円ぐらいか。といってもぎりぎりまで使った場合だけど。
他にも5GBまでのS3が無料だったり、色々とお得な様子。
MicroInstanceの範囲であればインスタンス代は無料なわけで、掛かるとしたらデータ転送料ぐらいか。
15GBまでは無料。それを超えると1GBに付き12円ぐらいなので100GBで1200円。
まぁ、まずは帯域を使うようなサービスを作らないと意味が無い計算だなぁ。
と、説明を見たらクレジットカード番号を入力して次ページへ。
電話番号を入力するとPINが表示され、同時に電話が掛かってきます。
当然のように英語の自動音声が流れますが気にせず携帯からPINを入力。
承認されるとWebページが自動的に切り替わります。フィードバックはえぇ。
次ページに進むと、再度色々なサービスの値段が表示される。とりあえず無視してComplete Sign Up
AWS Managing Console
WebベースでEC2のインスタンスを管理できるっぽい。
- まずはRegionをASIAに切り替え。
- Service HealthがAPAC - Singaporeになっているのを確認してLaunch Instance。
- Community AMIsを見るとUbuntuやらCentOSやら見えるけど、とりあえずはQuick Startで32bit Amazon Linux AMIを起動してみる
- Instance数は1個、Instance TypeからMicroを選択してLaunch Instances
- Advanced Instance OptionsでkernelやらRAMディスクやら選べるみたいだけどとりあえずそのままContinue
- Tag付けも別に不要なのでそのままContinue
- 鍵の作成。名前を入れると秘密鍵を生成してダウンロードしてくれる。同時にインスタンスのauthorized_keysに追加されてるのかな。
- SecurityGroupの作成。後で変えれば良いのでSSHだけ許可
- 設定内容が表示されるので確認してLaunch
10秒とかからずインスタンスが立ち上がる。
とりあえずCloseでウィンドウを閉じて見に行ってみる。
左メニューのInstancesから一覧を表示すると今作ったインスタンスが動いている。
Instance ActionからConnectを選ぶとSSHでの接続方法が表示される。
後は適当にいじってみる
・Puttyの場合、先にキーの変換をしないとなのでputtygen.exeを起動して変換
ppkファイルが出来たので説明にしたがって接続・・・
rootユーザじゃなくてec2-userでログインしてね。と切断された。・・・なぜWebの説明でrootユーザにしたし。
気を取り直してec2-userで再挑戦するとログイン成功。
アスキーアートでエンボス加工されたEC2の3文字が。目を細めると良く見える。
・とりあえずuname
[ec2-user@ip-10-130-59-18 ~]$ uname -a Linux ip-10-130-59-18 2.6.34.7-56.40.amzn1.i686 #1 SMP Fri Oct 22 18:48:33 UTC 2010 i686 i686 i386 GNU/Linux
・sudo権限もある
[ec2-user@ip-10-130-59-18 ~]$ sudo echo "hoge" hoge
・iptablesはからだった。てことはSecurityGroupでのSSH許可はインスタンスより上のレベルで制御されてるのか。
・ディスクは9Gぐらい空きあるけど、永続データはS3におくべきなんだっけ?
試しにファイルを作って再起動したら普通に残っていた。まぁ、プログラムとか毎回消えたらdeploy面倒だよね。
・VIMは7.0.237だった。
.vimrcは全くなかったのでいつものをコピペ。
でもよくあることでambiwidth=doubleにしていても記号がおかしかったり。しょぼん。