Node.jsまたはJavaScriptの非同期関数呼び出しを同期関数にラップする方法


122

関数を公開するライブラリを管理しているとしますgetData。ユーザーはそれを呼び出して実際のデータを取得します。
var output = getData();
内部ではデータがファイルに保存されるため、getDataNode.jsビルトインを使用して実装しますfs.readFileSync。それは明らかだ両方getDatafs.readFileSyncの同期機能があります。ある日、基礎となるデータソースを、非同期にのみアクセスできるMongoDBなどのリポジトリに切り替えるように言われました。また、ユーザーを怒らせるのを避けるように言われましたgetData。APIを変更して、Promiseのみを返すか、コールバックパラメータを要求することはできません。どのようにして両方の要件を満たしますか?

コールバック/約束を使用する非同期関数は、JavasSriptとNode.jsのDNAです。自明でないJSアプリはおそらくこのコーディングスタイルに浸透しています。しかし、この実践は、いわゆる運命のコールバックピラミッドに簡単につながる可能性があります。さらに悪いことに、呼び出しチェーン内の呼び出し元のコードが非同期関数の結果に依存している場合、それらのコードもコールバック関数にラップする必要があり、呼び出し元にコーディングスタイルの制約を課します。大規模なグローバルリファクタリングを回避するために、非同期関数(サードパーティのライブラリで提供されることが多い)を同期関数にカプセル化する必要がある場合があります。この問題に関する解決策の検索は、通常、ノードファイバーで終わりましたまたはそれから派生したnpmパッケージ。しかし、ファイバーは私が直面している問題を解決できません。ファイバーの作者が提供した例でさえ、欠陥を示していました。

...
Fiber(function() {
    console.log('wait... ' + new Date);
    sleep(1000);
    console.log('ok... ' + new Date);
}).run();
console.log('back in main');

実際の出力:

wait... Fri Jan 21 2011 22:42:04 GMT+0900 (JST)
back in main
ok... Fri Jan 21 2011 22:42:05 GMT+0900 (JST)

関数Fiberが非同期関数のスリープを本当に同期に変換する場合、出力は次のようになります。

wait... Fri Jan 21 2011 22:42:04 GMT+0900 (JST)
ok... Fri Jan 21 2011 22:42:05 GMT+0900 (JST)
back in main

JSFiddleで別の簡単な例を作成し、期待される出力を生成するコードを探しました。Node.jsでのみ機能するソリューションを受け入れるので、JSFiddleで機能していなくても、npmパッケージを自由に要求できます。


2
Nodeで非同期関数を同期させることはできません。同期できる場合でも、同期させないでください。問題は、fsモジュールで、ファイルシステムへの同期アクセスと非同期アクセスの完全に別々の関数を確認できることです。最善の方法は、非同期の外観をプロミスまたはコルーチン(ES6のジェネレーター)でマスクすることです。コールバックピラミッドを管理するには、関数呼び出しで定義するのではなく名前を付け、非同期ライブラリのようなものを使用します。
キューバイト2014

8
dandavisに対して、非同期は実装の詳細をコールチェーンにバブルアップし、グローバルなリファクタリングを強制する場合があります。これは、モジュール化と封じ込めが重要な複雑なアプリケーションにとって有害で​​あり、悲惨なことです。
abbr 2014

4
「運命のコールバックピラミッド」は、問題の表現にすぎません。Promiseはそれを隠したり偽装したりできますが、真の課題に対処することはできません。非同期関数の呼び出し元が非同期関数の結果に依存している場合、コールバックを使用する必要があり、呼び出し元も同様です。これは、制約を課す古典的な例です。単に実装の詳細のために呼び出し元。
abbr 2014

1
@abbr:deasyncモジュールのおかげで、問題の説明はまさに私が探していたものであり、実行可能な解決策を見つけることができませんでした。私はジェネレーターとイテラブルをいじりましたが、あなたと同じ結論に達しました。
Kevin Jhangiani

2
非同期機能を強制的に同期させることはほとんど決して良いことではないことに注意してください。あなたは、ほとんどの場合、まだ(シーケンシング、変数の設定、などなど)と同じ効果を達成しながら、そのまま機能の非同期ネスを維持し、よりよい解決策を持っています。
マダラのゴースト

回答:


104

deasyncは、JavaScript関数でNode.jsイベントループを呼び出すことにより、ブロッキングメカニズムで実装された非同期関数を同期に変換します。その結果、deasyncは、スレッド全体をブロックせず、ビジー待機を発生させずに、後続のコードの実行のみをブロックします。このモジュールを使用して、jsFiddleチャレンジの答えを次に示します。

function AnticipatedSyncFunction(){
  var ret;
  setTimeout(function(){
      ret = "hello";
  },3000);
  while(ret === undefined) {
    require('deasync').runLoopOnce();
  }
  return ret;    
}


var output = AnticipatedSyncFunction();
//expected: output=hello (after waiting for 3 sec)
console.log("output="+output);
//actual: output=hello (after waiting for 3 sec)

(免責事項:私はの共著者ですdeasync。このモジュールはこの質問を投稿した後に作成されたもので、有効な提案が見つかりませんでした。)


他の誰かがこれで運が良かったですか?動かせない。
ニューマン

3
ちゃんと動かせません。もっと使用したい場合は、このモジュールのドキュメントを改善する必要があります。著者がモジュールを使用することの影響を正確に知っているとは思えません。もしそうなら、文書化しません。
Alexander Mills

5
これまでのところ、Github Issue Trackerに文書化された1つの確認済みの問題があります。この問題はNode v0.12で修正されました。私が知っている残りは、文書化する価値のない根拠のない推測にすぎません。問題の原因が非同期であると思われる場合は、自己完結型の複製可能なシナリオを投稿してください。
abbr 2015年

私はそれを使おうとしました、そして私のスクリプトでいくつかの改善を得ました、それでも私は日付で運がありませんでした。私は次のようにコードを変更しfunction AnticipatedSyncFunction(){ var ret; setTimeout(function(){ var startdate = new Date() //console.log(startdate) ret = "hello" + startdate; },3000); while(ret === undefined) { require('deasync').runLoopOnce(); } return ret; } var output = AnticipatedSyncFunction(); var startdate = new Date() console.log(startdate) console.log("output="+output); ました。日付の出力に3秒の違いが表示されることを期待しています!
Alex

@abbrこれをブラウザ化してノードに依存せずに使用できます>
Gandhi

5

npm同期モジュールもあります。これは、クエリを実行するプロセスを同期するために使用されます。

並列クエリを同期的に実行したい場合、ノードは応答を待たないため、そのように制限します。同期モジュールは、この種のソリューションに最適です。

サンプルコード

/*require sync module*/
var Sync = require('sync');
    app.get('/',function(req,res,next){
      story.find().exec(function(err,data){
        var sync_function_data = find_user.sync(null, {name: "sanjeev"});
          res.send({story:data,user:sync_function_data});
        });
    });


    /*****sync function defined here *******/
    function find_user(req_json, callback) {
        process.nextTick(function () {

            users.find(req_json,function (err,data)
            {
                if (!err) {
                    callback(null, data);
                } else {
                    callback(null, err);
                }
            });
        });
    }

参照リンク:https : //www.npmjs.com/package/sync


4

機能ファイバーが非同期機能のスリープを本当に同期化する場合

はい。ファイバー内では、関数はロギングの前に待機しokます。ファイバーは非同期関数を同期化しませんが、非同期関数を使用して内で非同期的に実行される同期的に見えるコードを記述できFiberます。

大規模なグローバルリファクタリングを回避するために、非同期関数を同期関数にカプセル化する必要がある場合があります。

できません。非同期コードを同期させることは不可能です。グローバルコードでそれを予測し、最初から非同期スタイルで記述する必要があります。グローバルコードをファイバーでラップするか、promise、promiseジェネレーター、または単純なコールバックを使用するかは、設定​​によって異なります。

私の目的は、データ取得方法が同期から非同期に変更された場合の発信者への影響を最小限に抑えることです

約束と繊維の両方がそれを行うことができます。


1
これは、Node.jsで実行できる絶対的に最悪のことです。「非同期関数を使用し、非同期で実行される同期に見えるコード」です。あなたのAPIがそれをするなら、あなたは人生を台無しにするでしょう。非同期の場合はコールバックが必要であり、コールバックが提供されない場合はエラーがスローされます。あなたの目標が人々をだますことでない限り、それはAPIを作成する最良の方法です。
Alexander Mills

@AlexMills:はい、それは本当に恐ろしいことです。ただし、幸いなことに、これはAPIで実行できることではありません。非同期APIは常にコールバックを受け入れる/ promiseを返す/ファイバー内で実行されることを期待する必要があります-それなしでは機能しません。Afaik、ファイバーは主に、ブロックされていて同時実行性がなく、非同期APIを使用したいQuick'n'Dirtyスクリプトで使用されていました。ノードと同じように、同期fsメソッドを使用する場合があります。
Bergi

2
私は一般的にノードが好きです。特に、純粋なjsの代わりにtypescriptを使用できる場合。しかし、あなたが行うすべてに浸透し、単一の非同期呼び出しを行うことを決定するとすぐに、文字通りコールチェーン内のすべての関数に感染するこの全体的な非同期のナンセンスは、私が本当に嫌いなものです。Async apiは感染症のようなもので、1回の呼び出しでコードベース全体に感染し、すべてのコードを書き換える必要があります。私はこれが良いことだと誰もがどういう風に主張できるのか本当に理解していません。
クリス

@Kris Nodeは高速でシンプルなので、IOタスクに非同期モデルを使用します。多くのことを同期的に行うこともできますが、同時に何もできないため、ブロッキングは遅くなります。スレッドを作成しないと、すべてが複雑になります。
Bergi、

@Bergi私はマニフェストを読んだので、引数を知っています。しかし、既存のコードを、同等の同期を持たない最初のAPI呼び出しにヒットした瞬間に非同期に変更することは簡単ではありません。すべてが壊れ、すべてのコード行を精査する必要があります。あなたのコードが些細なものでない限り、私は保証します...全部を非同期イディオムに変換した後、変換して再び機能させるにはしばらく時間がかかります。
クリス、

2

あなたは約束を使わなければなりません:

const asyncOperation = () => {
    return new Promise((resolve, reject) => {
        setTimeout(()=>{resolve("hi")}, 3000)
    })
}

const asyncFunction = async () => {
    return await asyncOperation();
}

const topDog = () => {
    asyncFunction().then((res) => {
        console.log(res);
    });
}

アロー関数の定義がもっと好きです。ただし、「()=> {...}」という形式の文字列は、「関数(){...}」としても記述できます。

したがって、非同期関数を呼び出しても、topDogは非同期ではありません。

ここに画像の説明を入力してください

編集:非同期関数を同期関数内にラップする必要がある場合の多くは、コントローラー内にあることに気付きます。それらの状況のた​​めに、ここにパーティーのトリックがあります:

const getDemSweetDataz = (req, res) => {
    (async () => {
        try{
            res.status(200).json(
                await asyncOperation()
            );
        }
        catch(e){
            res.status(500).json(serviceResponse); //or whatever
        }
    })() //So we defined and immediately called this async function.
}

これをコールバックで利用すると、promiseを使用しないラップを行うことができます。

const asyncOperation = () => {
    return new Promise((resolve, reject) => {
        setTimeout(()=>{resolve("hi")}, 3000)
    })
}

const asyncFunction = async (callback) => {
    let res = await asyncOperation();
    callback(res);
}

const topDog = () => {
    let callback = (res) => {
        console.log(res);
    };

    (async () => {
        await asyncFunction(callback)
    })()
}

このトリックをEventEmitterに適用すると、同じ結果を得ることができます。コールバックを定義した場所にEventEmitterのリスナーを定義し、コールバックを呼び出した場所にイベントを発行します。


1

ノードファイバーでは解決できないシナリオが見つかりません。node-fibersを使用して提供した例は期待どおりに動作します。重要なのは、関連するすべてのコードをファイバー内で実行することです。そのため、ランダムな位置で新しいファイバーを開始する必要はありません。

例を見てみましょう:アプリケーションのエントリポイントであるフレームワークを使用するとします(このフレームワークは変更できません)。このフレームワークは、nodejsモジュールをプラグインとしてロードし、プラグインのいくつかのメソッドを呼び出します。このフレームワークは同期機能のみを受け入れ、ファイバー自体は使用しないとしましょう。

プラグインの1つで使用するライブラリがありますが、このライブラリは非同期であり、変更する必要もありません。

メインスレッドは、ファイバーが実行されていない場合は生成できませんが、ファイバーを使用してプラグインを作成できます。ファイバー内でフレームワーク全体を開始するラッパーエントリを作成するだけで、プラグインから実行を譲ることができます。

欠点:フレームワークの使用している場合setTimeoutPromise、内部の、それは繊維コンテキストをエスケープします。これはあざけることで回避することができsetTimeoutPromise.thenおよびすべてのイベントハンドラ。

したがって、これは、a Promiseが解決されるまでファイバーを生成する方法です。このコードは非同期(Promiseを返す)関数を取り、Promiseが解決されるとファイバーを再開します。

framework-entry.js

console.log(require("./my-plugin").run());

async-lib.js

exports.getValueAsync = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("Async Value");
    }, 100);
  });
};

my-plugin.js

const Fiber = require("fibers");

function fiberWaitFor(promiseOrValue) {
  var fiber = Fiber.current, error, value;
  Promise.resolve(promiseOrValue).then(v => {
    error = false;
    value = v;
    fiber.run();
  }, e => {
    error = true;
    value = e;
    fiber.run();
  });
  Fiber.yield();
  if (error) {
    throw value;
  } else {
    return value;
  }
}

const asyncLib = require("./async-lib");

exports.run = () => {
  return fiberWaitFor(asyncLib.getValueAsync());
};

my-entry.js

require("fibers")(() => {
  require("./framework-entry");
}).run();

実行node framework-entry.jsするとエラーがスローされますError: yield() called with no fiber running。実行するnode my-entry.jsと、期待どおりに動作します。


0

Node.jsコードの同期化は、データベースなどのいくつかの側面で不可欠です。しかし、Node.jsの実際の利点は非同期コードにあります。シングルスレッドなのでノンブロッキング。

重要な機能を使用して同期することができますFiber()await()を使用し、延期()await()を使用してすべてのメソッドを呼び出します。次に、コールバック関数をdefer()に置き換えます。

通常の非同期コード。これはCallBack関数を使用します。

function add (var a, var b, function(err,res){
       console.log(res);
});

 function sub (var res2, var b, function(err,res1){
           console.log(res);
    });

 function div (var res2, var b, function(err,res3){
           console.log(res3);
    });

Fiber()、await()、defer()を使用して上記のコードを同期します

fiber(function(){
     var obj1 = await(function add(var a, var b,defer()));
     var obj2 = await(function sub(var obj1, var b, defer()));
     var obj3 = await(function sub(var obj2, var b, defer()));

});

これがお役に立てば幸いです。ありがとうございました


0

現在、このジェネレーターパターンは多くの状況で解決策となります。

以下は、非同期のreadline.question関数を使用したnodejsの順次コンソールプロンプトの例です。

var main = (function* () {

  // just import and initialize 'readline' in nodejs
  var r = require('readline')
  var rl = r.createInterface({input: process.stdin, output: process.stdout })

  // magic here, the callback is the iterator.next
  var answerA = yield rl.question('do you want this? ', r=>main.next(r))    

  // and again, in a sync fashion
  var answerB = yield rl.question('are you sure? ', r=>main.next(r))        

  // readline boilerplate
  rl.close()

  console.log(answerA, answerB)

})()  // <-- executed: iterator created from generator
main.next()     // kick off the iterator, 
                // runs until the first 'yield', including rightmost code
                // and waits until another main.next() happens

-1

ファイバーを作成するコールの周囲で何が起こっいるのかを見ているのではなく、ファイバー内部で何が起こっいるのかを見るべきです。ファイバー内に入ると、同期スタイルでプログラムできます。例えば:

関数f1(){
    console.log( '待機...' +新しい日付);
    スリープ(1000);
    console.log( 'ok ...' + new Date);   
}

関数f2(){
    f1();
    f1();
}

ファイバー(関数(){
    f2();
})。run();

繊維内には、呼び出しf1f2そしてsleep彼らが同期であるかのように。

一般的なWebアプリケーションでは、HTTPリクエストディスパッチャーでファイバーを作成します。それが完了したら、非同期関数(fs、データベースなど)を呼び出す場合でも、すべての要求処理ロジックを同期スタイルで記述できます。


ブルーノに感謝します。しかし、サーバーがtcpポートにバインドする前に実行する必要があるブートストラップコードで同期スタイルが必要な場合-非同期で開かれているdbから読み取る必要がある構成やデータなど?私はserver.js全体をファイバーでラップすることになるかもしれません、そしてそれはプロセスレベル全体で並行性を殺すだろうと思います。それでも、確認する価値のある提案です。私にとって理想的なソリューションは、非同期関数をラップして同期呼び出し構文を提供し、プロセスレベルでの同時実行性を犠牲にすることなく、呼び出し側チェーンのコードの次の行のみをブロックできるようにすることです。
abbr 2014

ブートストラップコード全体を1つの大きなファイバー呼び出しでラップできます。ブートストラップコードは通常、リクエストの処理を開始する前に完了するまで実行する必要があるため、同時実行性は問題になりません。また、ファイバーは他のファイバーの実行を妨げません。yieldコールにヒットするたびに、他のファイバー(およびメインスレッド)に実行の機会を与えます。
Bruno Jouhier 14

Expressブートストラップファイルserver.jsをファイバーでラップしました。実行シーケンスは私が探しているものですが、そのラップは要求ハンドラに影響を与えません。したがって、同じラッパーを各ディスパッチャーに適用する必要があると思います。グローバルリファクタリングを回避するのに役立っていないようで、この時点で私はあきらめました。私の目的は、DAOレイヤーでデータ取得方法が同期から非同期に変更され、Fiberがまだ課題に達していない場合の発信者への影響を最小限に抑えることです。
abbr 2014

@fred:リクエストハンドラのようにイベントストリームを「同期」することはあまり意味がありません- while(true) handleNextRequest()ループが必要です。各リクエストハンドラをファイバーでラップすると、
ベルギ

@fred:Expressのコールバックがあるので、繊維がExpressでずっとあなたを助けにはなりませんではない継続コールバック(必ず1回だけ呼び出されるコールバック、エラーとまたは結果とのいずれか)。ただし、継続コールバック(fs、mongodb、その他多数)を備えた非同期APIの上に多数のコードを記述した場合、ファイバーは破滅のピラミッドを解決します。
Bruno Jouhier 14

-2

私は最初にnode.jsでこれに苦労しました。async.jsは、これに対処するのに役立つ最高のライブラリです。ノードで同期コードを記述したい場合は、この方法を使用します。

var async = require('async');

console.log('in main');

doABunchOfThings(function() {
  console.log('back in main');
});

function doABunchOfThings(fnCallback) {
  async.series([
    function(callback) {
      console.log('step 1');
      callback();
    },
    function(callback) {
      setTimeout(callback, 1000);
    },
    function(callback) {
      console.log('step 2');
      callback();
    },
    function(callback) {
      setTimeout(callback, 2000);
    },
    function(callback) {
      console.log('step 3');
      callback();
    },
  ], function(err, results) {
    console.log('done with things');
    fnCallback();
  });
}

このプログラムは常に以下を生成します...

in main
step 1
step 2
step 3
done with things
back in main

2
asyncあなたの例で動作しますb / c it's main、これは呼び出し元を気にしません。すべてのコードが、非同期関数呼び出しの1つの結果を返すことになっている関数にラップされていると想像してください。console.log('return');コードの最後に追加することで、機能しないことを簡単に証明できます。このような場合、の出力はの前にreturn発生します。in mainstep 1
abbr

-11

Javascriptはシングルスレッド言語であり、サーバー全体をブロックする必要はありません。非同期コードは、依存関係を明示的にすることにより、競合状態を排除します。

非同期コードを愛することを学んでください!

promisesコールバック地獄のピラミッドを作成せずに非同期コードを見てください。node.jspromiseQライブラリをお勧めします

httpGet(url.parse("http://example.org/")).then(function (res) {
    console.log(res.statusCode);  // maybe 302
    return httpGet(url.parse(res.headers["location"]));
}).then(function (res) {
    console.log(res.statusCode);  // maybe 200
});

http://howtonode.org/promises

編集:これは断然私の最も物議を醸している答えです、ノードにはyieldキーワードがあり、非同期コードを同期であるかのように扱うことができます。http://blog.alexmaccaw.com/how-yield-will-transform-node


1
Promiseは、関数を同期させるのではなく、コールバックパラメータを言い換えるだけです。
abbr 2014

2
yuu同期しないでください。同期しないと、サーバー全体がブロックされます。stackoverflow.com/questions/17959663/...
roo2

1
望ましいのは、Node.jsによって処理されている別のリクエストなど、他のイベントをブロックしない同期呼び出しです。定義によるSync関数は、結果が生成されるまで(単なるPromiseではなく)呼び出し元に戻らないことのみを意味します。呼び出しがブロックされている間、サーバーが他のイベントを処理することを事前に排除するものではありません。
abbr 2014

@fred:約束のポイントを逃していると思います。これらは単なるオブザーバーパターンの抽象化ではなく、非同期アクションをチェーンして構成する方法を提供します。
ベルギ

1
@ベルギ、私はプロミスをよく使い、それが何をするかを正確に知っています。効果的に達成されるのは、単一の非同期関数呼び出しを複数の呼び出し/ステートメントに分解することだけです。しかし、それは結果を変更しません-呼び出し元が戻ったとき、非同期関数の結果を返すことはできません。JSFiddleに投稿した例を確認してください。その場合の呼び出し元は関数AnticipatedSyncFunctionで、非同期関数はsetTimeoutです。promiseを使用して私の課題に答えられる場合は、見せてください。
2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.