12

最初に問題の一般的な説明を試みた後、通常のアプローチが機能しない理由をさらに詳しく説明しました。これらの抽象化された説明を読みたい場合は、続けてください。最後に、より大きな問題と具体的なアプリケーションについて説明しますので、それを読みたい場合は、「実際のアプリケーション」にジャンプしてください。

node.js 子プロセスを使用して、計算集約的な作業を行っています。親プロセスは機能しますが、実行のある時点で、続行する前に子プロセスからの情報を取得する必要があるポイントに到達します。したがって、子プロセスが終了するのを待つ方法を探しています。

私の現在のセットアップは次のようになります。

importantDataCalculator = fork("./runtime");
importantDataCalculator.on("message", function (msg) {
    if (msg.type === "result") {
        importantData = msg.data;
    } else if (msg.type === "error") {
        importantData = null;
    } else {
        throw new Error("Unknown message from dataGenerator!");
    }
});

そしてどこか

function getImportantData() {
    while (importantData === undefined) {
        // wait for the importantDataGenerator to finish
    }

    if (importantData === null) {
        throw new Error("Data could not be generated.");
    } else {
        // we should have a proper data now
        return importantData;
    }
}

したがって、親プロセスが開始されると、コードの最初のビットが実行され、子プロセスが生成されてデータが計算され、独自の作業が続けられます。継続するために子プロセスからの結果が必要になると、 を呼び出しますgetImportantData()。したがって、getImportantData()データが計算されるまでブロックするという考え方です。

しかし、私が使用した方法は機能しません。これは、while ループを使用してイベント ループが実行されないようにしたためだと思います。そして、Event-Loop が実行されないため、子プロセスからのメッセージを受信できず、while-loop の状態が変化せず、無限ループになります。

もちろん、私はこの種の while ループを使いたくありません。私がやりたいことは、node.js に「イベント ループの 1 回の反復を実行してから、戻ってきてください」と伝えることです。必要なデータが受信されるまでこれを繰り返し実行し、ゲッターから戻って実行を続けます。

彼が同じ関数に何度か再入する危険性があることは理解していますが、これを使用したいモジュールは、子プロセスからのこのメッセージを待機し、進行状況を報告する他のメッセージを送信することを除いて、イベントループでほとんど何もしません。それで問題ないはずです。

Node.jsでイベントループを1回だけ実行する方法はありますか? または、同様のことを達成する別の方法はありますか? それとも、私がここでやろうとしていることを達成するためのまったく異なるアプローチがありますか?

これまでのところ、私が考えることができる唯一の解決策は、さらに別のプロセスを導入するように計算を変更することです。このシナリオでは、重要なデータを計算するプロセス、重要なデータが不要なデータのビットを計算するプロセス、およびこれら 2 つの親プロセスがあり、2 つの子プロセスからのデータを待って結合します。彼らが到着したときの作品。それ自体で計算集約的な作業を行う必要がないため、イベント ループ (= メッセージ) からのイベントを待機してそれらに反応し、必要に応じて結合されたデータを転送し、まだ結合できないデータの断片を格納することができます。ただし、これによりさらに別のプロセスが導入され、プロセス間通信がさらに増えるため、オーバーヘッドが増えるため、これは避けたいと思います。

編集

詳細が必要であることがわかります。

親プロセス (プロセス 1 と呼びましょう) は、それ自体が別のプロセス (プロセス 0) によって生成されたプロセスであり、計算集約的な作業を行います。実際には、私が制御できないコードを実行するだけなので、非同期で動作させることはできません。私ができること (そして実行したこと) は、定期的に実行されるコードに関数を呼び出して、進行状況を報告し、部分的な結果を提供することです。この進捗レポートは、IPC を介して元のプロセスに送り返されます。

ただし、まれに部分的な結果が正しくない場合があるため、修正する必要があります。そのためには、通常の計算とは別に計算できるデータが必要です。ただし、この計算には数秒かかる場合があります。したがって、別のプロセス (プロセス 2) を開始してこの計算を行い、IPC メッセージを介してプロセス 1 に結果を提供します。現在、プロセス 1 と 2 は喜んで計算を行っており、プロセス 2 で計算された補正データは、プロセス 1 が必要とする前に終了していることを願っています。しかし、プロセス 1 の初期の結果の 1 つを修正する必要がある場合があり、その場合、プロセス 2 が計算を完了するのを待たなければなりません。プロセス 1 のイベント ループをブロックしても、メイン プロセス (プロセス 0) は影響を受けないため、理論的には問題ありません。唯一の問題は、

そのため、イベント ループをブロックせずに、プロセス 1 のコードの実行を一時停止する必要があります。process.runEventLoopIterationイベントループの反復を実行してから戻るような呼び出しがあることを望んでいました。

次に、次のようにコードを変更します。

function getImportantData() {
    while (importantData === undefined) {
        process.runEventLoopIteration();
    }

    if (importantData === null) {
        throw new Error("Data could not be generated.");
    } else {
        // we should have a proper data now
        return importantData;
    }
}

したがって、必要なデータを受け取るまでイベントループを実行しますが、getImportantData() を呼び出したコードの実行は継続しません。

基本的に私がプロセス1でやっていることはこれです:

function callback(partialDataMessage) {
    if (partialDataMessage.needsCorrection) {
        getImportantData();
        // use data to correct message
        process.send(correctedMessage); // send corrected result to main process
    } else {
        process.send(partialDataMessage); // send unmodified result to main process
    }
}

function executeCode(code) {
    run(code, callback); // the callback will be called from time to time when the code produces new data
    // this call is synchronous, run is blocking until the calculation is finished
    // so if we reach this point we are done
    // the only way to pause the execution of the code is to NOT return from the callback 
}

実際の適用・実装・問題点

次のアプリケーションでは、この動作が必要です。これを達成するためのより良いアプローチがある場合は、遠慮なく提案してください。

任意のコードを実行し、変更された変数、呼び出された関数、発生した例外などについて通知を受けたいと考えています。収集した情報を UI の横に表示するには、コード内のこれらのイベントの場所も必要です。オリジナルコード。

これを実現するために、コードを計測し、コールバックを挿入します。次に、コードを実行し、その実行を try-catch ブロックでラップします。コールバックが実行に関する何らかのデータ (変数の変更など) で呼び出されるたびに、変更について知らせるメッセージをメイン プロセスに送信します。このようにして、実行中にコードの実行についてユーザーに通知されます。これらのコールバックによって生成されたイベントの位置情報は、インストルメンテーション中にコールバック呼び出しに追加されるため、問題はありません。

例外が発生すると問題が発生します。また、テストされたコードの例外についてユーザーに通知したいと考えています。そのため、コードの実行を try-catch でラップし、実行から出た例外をキャッチしてユーザー インターフェイスに送信します。しかし、エラーの場所が正しくありません。node.js によって作成された Error オブジェクトには完全なコール スタックがあるため、エラーが発生した場所がわかります。ただし、この場所はインストルメント化されたコードに対して相対的であるため、この場所情報をそのまま使用して、元のコードの横にエラーを表示することはできません。インストルメント化されたコード内のこの場所を元のコード内の場所に変換する必要があります。そのために、コードを計測した後、ソース マップを計算します。インストルメント化されたコード内の場所を元のコード内の場所にマップします。ただし、この計算には数秒かかる場合があります。そこで、インストルメント化されたコードの実行が既に開始されている間に、子プロセスを開始してソース マップを計算することにしました。次に、例外が発生したときに、ソース マップが既に計算されているかどうかを確認し、計算されていない場合は、位置を修正できるように計算が完了するのを待ちます。

実行および監視されるコードは完全に任意である可能性があるため、非同期に簡単に書き直すことはできません。私はそれが提供されたコールバックを呼び出すことだけを知っています. また、メッセージを保存してコードの実行を続行するために戻って、次の呼び出し中にソース マップが終了したかどうかを確認することもできません。これは、コードの実行を続行するとイベント ループもブロックされ、計算されたソースが妨げられるためです。 map は、実行プロセスで受信されることはありません。または、受信した場合は、実行するコードが完全に終了した後でのみ、かなり遅くなるか、まったく終了しない可能性があります (実行するコードに無限ループが含まれている場合)。しかし、sourceMap を受け取る前に、実行状態に関する更新をさらに送信することはできません。組み合わせて、

イベント ループに制御を一時的に明け渡すことで、この問題は解決します。しかし、それは不可能のようです。もう 1 つのアイデアは、実行プロセスと sourceMapGeneration プロセスの両方を制御する 3 番目のプロセスを導入することです。実行プロセスから進捗メッセージを受信し、修正が必要なメッセージがある場合は、sourceMapGeneration プロセスを待ちます。プロセスは独立しているため、制御プロセスは受信したメッセージを保存し、sourceMapGeneration プロセスが実行されるまで待機することができます。その間、実行プロセスは実行を続けます。ソース マップを受信するとすぐに、メッセージを修正してすべてを送信します。

ただし、これにはさらに別のプロセス(オーバーヘッド)が必要になるだけでなく、プロセス間でコードをもう一度転送する必要があることも意味します。コードには何千もの行が含まれている可能性があり、それ自体に時間がかかる可能性があるため、移動したいと思いますできるだけ少なくします。

これで、通常の「非同期コールバック」アプローチを使用できない、または使用しなかった理由が説明されることを願っています。

4

4 に答える 4

6

3 番目(:)) の解決策を問題に追加した後、どのような動作を求めるかを明確にした後、 Fibersを使用することをお勧めします。

ファイバーを使用すると、 nodejsでコルーチンを実行できます。コルーチンは、複数の入口/出口ポイントを許可する関数です。これは、制御を譲り、好きなように再開できることを意味します。

これは、sleep正確にそれを行う公式ドキュメントの関数であり、一定時間スリープしてアクションを実行します。

function sleep(ms) {
    var fiber = Fiber.current;
    setTimeout(function() {
        fiber.run();
    }, ms);
    Fiber.yield();
}

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

リソースを待機するコードを関数に配置して、タスクが完了したときにリソースを解放し、再度実行することができます。

たとえば、質問から例を適応させます。

var pausedExecution, importantData;
function getImportantData() {
    while (importantData === undefined) {
        pausedExecution = Fiber.current;
        Fiber.yield();
        pausedExecution = undefined;
    }

    if (importantData === null) {
        throw new Error("Data could not be generated.");
    } else {
        // we should have proper data now
        return importantData;
    }
}

function callback(partialDataMessage) {
    if (partialDataMessage.needsCorrection) {
        var theData = getImportantData();
        // use data to correct message
        process.send(correctedMessage); // send corrected result to main process
    } else {
        process.send(partialDataMessage); // send unmodified result to main process
    }
}

function executeCode(code) {
    // setup child process to calculate the data
    importantDataCalculator = fork("./runtime");
    importantDataCalculator.on("message", function (msg) {
        if (msg.type === "result") {
            importantData = msg.data;
        } else if (msg.type === "error") {
            importantData = null;
        } else {
            throw new Error("Unknown message from dataGenerator!");
        }

        if (pausedExecution) {
            // execution is waiting for the data
            pausedExecution.run();
        }
    });


    // wrap the execution of the code in a Fiber, so it can be paused
    Fiber(function () {
        runCodeWithCallback(code, callback); // the callback will be called from time to time when the code produces new data
        // this callback is synchronous and blocking,
        // but it will yield control to the event loop if it has to wait for the child-process to finish
    }).run();
}

幸運を!私はいつも、3 つの問題を同じ方法で解決するよりも、1 つの問題を 3 つの方法で解決する方が良いと言っています。私はあなたのために働く何かを解決できたことをうれしく思います. 確かに、これはかなり興味深い質問でした。

于 2013-05-05T10:43:44.697 に答える
5

非同期プログラミングのルールは、非同期コードを入力したら、引き続き非同期コードを使用する必要があるということです。またはそのようなものを介して何度も関数を呼び出し続けることができますが、非同期プロセスからsetImmediateしようとしているという問題がまだあります。return

あなたのプログラムについて詳しく知らなければ、プログラムをどのように構築すべきかを正確に説明することはできませんが、概して、非同期コードを含むプロセスからデータを「返す」方法は、コールバックを渡すことです。おそらくこれはあなたを正しい軌道に乗せるでしょう:

function getImportantData(callback) {
    importantDataCalculator = fork("./runtime");
    importantDataCalculator.on("message", function (msg) {
        if (msg.type === "result") {
            callback(null, msg.data);
        } else if (msg.type === "error") {
            callback(new Error("Data could not be generated."));
        } else {
            callback(new Error("Unknown message from sourceMapGenerator!"));
        }
    });
}

次に、この関数を次のように使用します。

getImportantData(function(error, data) {
    if (error) {
        // handle the error somehow
    } else {
        // `data` is the data from the forked process
    }
});

これについては、スクリーンキャストの 1 つであるThinking Asynchronouslyでもう少し詳しく説明しています。

于 2013-05-04T19:40:22.927 に答える
1

あなたの質問 (更新) は非常に興味深いものです。例外を非同期にキャッチする際に発生した問題と密接に関連しているようです。(また、ブランドンと私はそれについて私と興味深い議論をしました!それは小さな世界です)

例外を非同期的にキャッチする方法については、この質問を参照してください。重要な概念は、(nodejs 0.8+ を想定して) nodejs ドメインを使用して例外の範囲を制限できるということです。

これにより、非同期ブロックを で囲むことができるため、例外の場所を簡単に取得できますatry/catch。これにより、ここでより大きな問題が解決されるはずだと思います。

リンクされた質問で関連するコードを見つけることができます。使用法は次のようなものです。

atry(function() {
    setTimeout(function(){
        throw "something";
    },1000);
}).catch(function(err){
    console.log("caught "+err);
});

のスコープにアクセスatryできるので、そこでスタック トレースを取得できます。これにより、より複雑なソース マップの使用をスキップできます。

幸運を!

于 2013-05-04T21:40:47.040 に答える