この問題を解決する私が見つけたデザインを使用できます。この設計では、イベント ドリブン プログラムを想定しています (ただし、そうでない場合は、偽のイベント ループを作成できます)。
わかりやすくするために、特定の問題については忘れて、代わりに 2 つのオブジェクト間のインターフェイスの問題に注目しましょう。送信側オブジェクトがデータ パケットを受信側オブジェクトに送信します。送信者は常に、受信者がデータ パケットの処理を終了するのを待ってから、別のデータ パケットを送信する必要があります。インターフェイスは、次の 2 つの呼び出しによって定義されます。
- Send() - データ パケットの送信を開始するために送信者によって呼び出され、受信者によって実装されます
- Done() - 送信操作が完了し、さらにパケットを送信できることを送信者に通知するために、受信者によって呼び出されます
これらの呼び出しはどれも何も返しません。受信者は、Done() を呼び出して、常に操作の完了を報告します。ご覧のとおり、このインターフェイスは概念的には提示されたものと似ており、Send() と Done() の間の再帰という同じ問題があり、スタック オーバーフローが発生する可能性があります。
私の解決策は、ジョブ キューをイベント ループに導入することでした。ジョブ キューは、ディスパッチされるのを待っているイベントのLIFO キュー(スタック) です。イベント ループは、キューの一番上にあるジョブを最優先イベントとして扱います。つまり、イベント ループがディスパッチするイベントを決定する必要がある場合、ジョブ キューが空でない場合は常にジョブ キューの一番上のジョブをディスパッチし、他のイベントはディスパッチしません。
次に、上記のインターフェイスを変更して、Send() と Done() の両方の呼び出しをqueuedにします。つまり、送信者が Send() を呼び出すと、ジョブがジョブ キューにプッシュされ、このジョブがイベント ループによってディスパッチされると、受信者の Send() の実際の実装が呼び出されます。Done() も同じように動作します - 受信者によって呼び出され、ディスパッチされると送信者の Done() の実装を呼び出すジョブをプッシュするだけです。
キューの設計がどのように 3 つの主要な利点を提供するかをご覧ください。
Send() と Done() の間に明示的な再帰がないため、スタック オーバーフローが回避されます。ただし、送信側は Done() コールバックから直接 Send() を呼び出すことができ、受信側は Send() コールバックから直接 Done() を呼び出すことができます。
これにより、すぐに完了した (I/O) 操作と、ある程度時間がかかる操作 (つまり、受信側がシステム レベルのイベントを待機する必要がある) との違いが曖昧になります。たとえば、ノンブロッキング ソケットを使用する場合、受信側の Send() の実装は send() syscall を呼び出します。これにより、何らかの送信が行われるか、または EAGAIN/EWOULDBLOCK が返されます。この場合、受信側はイベント ループに通知を要求します。ソケットが書き込み可能な場合。ソケットが書き込み可能であることがイベント ループによって通知されると、send() syscall を再試行します。これはおそらく成功します。この場合、このイベント ハンドラから Done() を呼び出して操作が完了したことを送信者に通知します。どちらが発生しても、送信者の観点からは同じです。Done() 関数は、送信操作が完了すると、すぐに、またはしばらくしてから呼び出されます。
これにより、実際の I/O と直交するエラー処理が行われます。エラー処理は、何らかの方法でエラーを処理する Error() コールバックを受信者に呼び出させることで実装できます。送信者と受信者が、エラーについて何も知らない独立した再利用可能なモジュールになる方法を確認してください。エラーが発生した場合 (たとえば、send() syscall が EAGAIN/EWOULDBLOCK ではなく実際のエラー コードで失敗した場合)、送信側と受信側は Error() コールバックから簡単に破棄できます。これは、送信側を作成した同じコードの一部である可能性があります。とレシーバー。
これらの機能を組み合わせることで、イベント駆動型プログラムでエレガントなフローベースのプログラミングが可能になります。BadVPNソフトウェア プロジェクトにキューの設計とフローベースのプログラミングを実装し、大きな成功を収めました。
最後に、ジョブ キューを LIFO にする必要がある理由について説明します。LIFO スケジューリング ポリシーは、ジョブのディスパッチの順序を大まかに制御します。たとえば、あるオブジェクトのメソッドを呼び出していて、このメソッドが実行された後、プッシュされたすべてのジョブが再帰的にディスパッチされた後に何かをしたいとします。このメソッドを呼び出す前に独自のジョブをプッシュし、このジョブのイベント ハンドラーから作業を行うだけです。
ジョブをデキューすることで、この延期された作業をいつでもキャンセルできるという優れたプロパティもあります。たとえば、この関数が実行した何か (プッシュされたジョブを含む) によってエラーが発生し、結果として独自のオブジェクトが破壊された場合、デストラクタはプッシュしたジョブをデキューして、ジョブが実行されてデータにアクセスした場合に発生するクラッシュを回避できます。それはもはや存在しません。