Quantcast
Viewing all articles
Browse latest Browse all 8902

nodejs v12(LTS)におけるasync, awaitを用いたstream処理

nodejs v12(LTS)におけるasync, awaitを用いたstream処理

QualiArts Advent Calendar 2019、3日目の記事になります。

はじめに

2019年10月21日にnodejs v12のLTS版が公開されました。

nodeは奇数バージョンが開発版、偶数バージョンが安定版となるため、v11以降の今まで実プロジェクトだと利用しにくかった機能がこれによりいくつか使えるようになりました。

そのなかでもasync-generatorsやfor-await-of loops構文が大手を振って使えるようになったことにより、stream関連の処理が大きく変わると感じたため、すでにいくつか紹介されている記事も有りますが、この機会に改めて紹介したいと思います。
また、最後に簡単なサンプル処理も記述しているので、ご参考いただければ幸いです。

for-await-of loops

今までstreamはeventEmitterを利用し、発火したeventをトリガーに処理を記述していました。
for-await-of loopsを用いると下記のようにわかりやすくかけるようになります。
for-await-of自体は単純にfor-ofのasyncも利用可能になったものとなります。

for-await-of_loops1.js
constfs=require('fs');constreader=async(rs)=>{forawait(constchunkofrs){console.log(chunk);}};(asyncfunction(){constrs=fs.createReadStream('./input.txt',{encoding:'utf8'});awaitreader(rs);})();
input.txt
abcde
fghij
klmno
pqrxy
z

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

従来だと、下記のように終了イベントをpromise化するなどで、全体のpromise化などは簡単にできますが、イテレータ内部のchunk単位でpromise化を行う場合非常に可読性が悪くなってしまっていました。
(v10以降であれば下記のようにstream.finishedをpromisifyすることで全体のpromise化は簡略可能です)

これが上記のように簡単に記述できるようになったのは非常にやりやすくなったと感じます。

for-await-of_loops2.js
constfs=require('fs');conststream=require('stream');constutil=require('util');constfinished=util.promisify(stream.finished);constmsleep=util.promisify(setTimeout);// streamを用いた場合の処理(全体終了部分のみのpromise化)constreader1=async(rs)=>{rs.on('data',(chunk)=>{console.log(chunk);});awaitfinished(rs);// stream.finishedを使わない場合下記のようなpromiseを生成する// await new Promise((resolve, reject) => {//   rs.once('finished', (chunk) => {//     return resolve(data);//   });//   rs.once('error', (err) => {//     return reject(err);//   });// });};// streamを用いた場合の処理(chunk単位でのpromise化)constreader2=async(rs,iterator)=>{letbuffer='';rs.on('data',(chunk)=>{buffer=chunk;rs.pause();});letisEnd=false;rs.once('end',()=>{isEnd=true;});leterror;rs.once('error',(err)=>{error=err;});while(true){if(error){throwerror;}if(buffer){awaititerator(buffer);buffer='';}elseif(isEnd){return;}elseif(rs.isPaused()){rs.resume();}// 非同期メソッドがないと無限ループしてしまうためawaitmsleep(0);}}(asyncfunction(){constrs1=fs.createReadStream('./input.txt',{encoding:'utf8'});awaitreader1(rs1);constrs2=fs.createReadStream('./input.txt',{encoding:'utf8'});awaitreader2(rs2,async(chunk)=>{console.log(chunk);});})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z
abcde
fghij
klmno
pqrxy
z

async-generators

今までは同期メソッドでしか使えなかったyieldがasyncにも対応しました。
async function*でasyncIteratorのジェネレータメソッドを生成でき、await対応したnextメソッドを呼び出すことができます。
(nextで呼び出した場合返り値はObjectになります)

async-generators1.js
constutil=require('util');constmsleep=util.promisify(setTimeout);asyncfunction*generate(){for(leti=1;i<=3;i++){awaitmsleep(1000);yieldi;}}constasyncIterator=generate();(async()=>{console.log(awaitasyncIterator.next());console.log(awaitasyncIterator.next());console.log(awaitasyncIterator.next());console.log(awaitasyncIterator.next());})();

実行結果

{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }

こちらはfor-await-of loopsも利用可能です。
こちらを利用すると簡単にラグのあるstreamデータの生成が可能になります。

async-generators2.js
constutil=require('util');constmsleep=util.promisify(setTimeout);asyncfunction*generate(){for(leti=1;i<=3;i++){awaitmsleep(1000);yieldi;}}constasyncIterator=generate();(async()=>{forawait(constvofasyncIterator){console.log(v);}})();

実行結果

1
2
3

行ごとにawait処理を行うサンプル

上記の機能が実装されたことで、行ごとのように一定windowずつstreamで非同期メソッドを実行する処理が非常に簡単にかけるようになりました。
下記は得られたstreamを、行ごとに非同期メソッドを実行する場合のサンプルになります。
可読性重視&行単位でeventループが回るため、パフォーマンスがシビアな場合は別途実装することをおすすめします。

readlineモジュールを利用した場合

line-reader1.js
constfs=require('fs');constreadline=require('readline');constutil=require('util');constmsleep=util.promisify(setTimeout);constasyncLineReader=async(iterater)=>{constrl=readline.createInterface({input:fs.createReadStream('input.txt',{encoding:'utf8'}),crlfDelay:Infinity});forawait(constlineofrl){awaititerater(line);}}(async()=>{awaitasyncLineReader(async(line)=>{awaitmsleep(100);console.log(line);});})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

解説

こちらはreadlineモジュールを利用したものになります。
以前も行ごとに処理を行えたのですが、非同期メソッドの実行はできませんでした。
v1.12からasyncIteratorに対応したことで、上記のように簡単に非同期メソッドが実行できるようになりました。

stream(for-await-of利用)

line-reader2.js
constfs=require('fs');constutil=require('util');constmsleep=util.promisify(setTimeout);// streamを用いた場合の処理(for-await-of使用)constasyncLineReader=async(iterater)=>{constrs=fs.createReadStream('./input.txt',{encoding:'utf8'});letbuffer='';forawait(constchunkofrs){buffer+=chunk;constlist=buffer.split('\n');// 最後の要素は改行が含まれているわけではないため、bufferに戻すbuffer=list.pop();for(leti=0;i<list.length;i++){awaititerater(list[i]);}}if(buffer){// 終了時にbufferに残っている文字列もiteratorにわたすawaititerater(buffer);}}(async()=>{awaitasyncLineReader(async(line)=>{awaitmsleep(100);console.log(line);});})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

解説

こちらはstreamのfor-await-ofを利用したものになります。
イベントループの回数が他と比べて半分以下なので、このなかでは一番パフォーマンスが良いです。
実装を合わせるために1行ずつ処理していますが、こちらで複数行制御してlistをiteratorに渡すような実装が実利用だと良いかもしれません。
比較的簡単に可読性良く記述できるようになっているかと思います。

stream(for-await-of未使用)

line-reader2.js
constfs=require('fs');constutil=require('util');constmsleep=util.promisify(setTimeout);// streamを用いた場合の処理(for-await-of未使用)constasyncLineReader=async(iterator)=>{constrs=fs.createReadStream('./input.txt',{encoding:'utf8'});letbuffer='';letrows=[];rs.on('data',(chunk)=>{buffer+=chunk;constlist=buffer.split('\n');buffer=list.pop();if(list.length){rows.push(...list);rs.pause();}});letisEnd=false;rs.once('end',()=>{isEnd=true;});leterror;rs.once('error',(err)=>{error=err;});while(true){if(error){// errorがあれば終了throwerror;}if(rows.length){for(leti=0;i<rows.length;i++){awaititerator(rows[i]);}rows=[];}elseif(isEnd){if(buffer){// 終了時にbufferに残っている文字列もiteratorにわたすawaititerator(buffer);}return;}elseif(rs.isPaused()){rs.resume();}// 非同期メソッドがないと無限ループしてしまうため、setImmediate代わりに実行awaitmsleep(0);}}(async()=>{awaitasyncLineReader(async(line)=>{awaitmsleep(100);console.log(line);});})();

実行結果

terminal
abcde
fghij
klmno
pqrxy
z

解説

こちらはstreamのfor-await-ofの未使用版になります。
かなり複雑になり可読性も落ちている事がわかります。
ただ、async, awaitが対応しているv8以降であれば利用可能なため、nodeのバージョン次第では利用できるかもしれません。

まとめ

これらの機能の追加により、async, awaitを用いたstream処理が非常に簡潔に記述できるようになりました。
v1.12LTSに上げることで非常にstream周りが記述しやすくなっているため、この機会にぜひ試してみてはどうでしょうか?


Viewing all articles
Browse latest Browse all 8902