非同期プログラミング:遅延コンセプト

非同期プログラミングの概念は、関数の結果がすぐに利用できるのではなく、しばらくしてから非同期(通常の実行順序に違反する)呼び出しの形で利用できることです。 なぜこれが役立つのでしょうか? いくつかの例を見てみましょう。



最初の例は、ネットワークサーバー、Webアプリケーションです。 ほとんどの場合、このようなアプリケーションはプロセッサで計算を実行しません。 ほとんどの時間(実際の非プロセッサ)はI / Oに費やされます:クライアントからのリクエストの読み取り、データ用のディスクへのアクセス、他のサブシステム(データベース、キャッシュサーバー、RPCなど)へのネットワークアクセス、クライアントへの応答の記録。 これらのI / O操作中、プロセッサはアイドル状態であり、他のクライアントからの要求を処理することでロードできます。 この問題を解決するにはさまざまな方法があります:各接続の個別プロセス( Apache mpm_prefork、 PostgreSQLPHP FastCGI)、各接続の個別スレッド(スレッド)、またはプロセス/スレッドの組み合わせバージョン( Apache mpm_worker、 MySQL )。 プロセスまたはスレッドを使用するアプローチは、OSで処理された接続間でプロセッサの多重化をシフトしますが、比較的多くのリソース(メモリ、コンテキストスイッチングなど)を消費します。このオプションは、多数の同時接続の処理には適しませんが、計算量は非常に多くなります(DBMSなど)。 スレッドとプロセスモデルの利点に、マルチプロセッサアーキテクチャで利用可能なすべてのプロセッサの潜在的な使用を追加できます。



別の方法は、OSが提供する非同期I / Oプリミティブ(選択、ポーリングなど)を使用してシングルスレッドモデルを使用することです。 さらに、新しいサービス対象接続ごとのリソースの量はそれほど大きくありません(新しいソケット、アプリケーションメモリ内の一部の構造)。 ただし、プログラミングははるかに複雑です。 ネットワークソケットからのデータは「スニペット」に含まれ、さらに、1つの処理サイクルで、データは異なる状態の異なる接続から来ます。接続の一部はクライアントから着信でき、一部は外部リソース(DB、他のサーバーなど)に発信できます。 ) 開発を簡素化するために、コールバック、ステートマシンなどのさまざまな概念が使用されます。 非同期I / Oを使用するネットワークサーバーの例: nginxlighttpdHAProxypgBouncerなど。 このようなシングルスレッドモデルでは、非同期プログラミングが必要です。 たとえば、データベースでクエリを実行します。 プログラムの観点から見ると、リクエストの実行はネットワークI / Oです。サーバーへの接続、リクエストの送信、応答の待機、データベースサーバーからの応答の読み取りです。 したがって、「データベースクエリを実行する」関数を呼び出すと、すぐに結果を返すことはできません(そうでない場合はブロックする必要があります)が、その後クエリ結果を受け取るものだけを返すか、おそらくエラー(サーバーへの接続がなく、不正です)リクエストなど)この戻り値でDeferredを作成すると便利です。



2番目の例は、従来のデスクトップアプリケーションの開発に関連しています。 ミランダの類似物( QIPMDC 、...)、つまり私たち自身のメッセンジャーを作成することにしたと仮定します。 プログラムインターフェイスには、連絡先を削除できる連絡先リストがあります。 ユーザーがこのアクションを選択すると、連絡先が画面に表示されなくなり、実際に連絡先リストから削除されます。 実際、サーバーの連絡先リストから削除する操作は、サーバーとのネットワーク相互作用に依存していますが、ユーザーインターフェイスはこの操作の間ブロックされるべきではないため、いずれの場合でも、操作が完了した後、操作の結果との非同期の相互作用が必要になります シグナルスロット、コールバックなどのメカニズムを使用できますが、Deferredが最適です。連絡先リストから削除する操作はDeferredを返し、Deferredを返します。これは、肯定的な結果(すべて順調)または例外(正確なエラー、ユーザーに通知する必要があります):エラーが発生した場合、連絡先リストの連絡先に連絡先を復元する必要があります。



延期されたものについての例は、長い間、たくさん与えられます。 Deferredは、 Twisted Pythonの非同期ネットワークプログラミングフレームワークの中心です。 これはシンプルで一貫性のある概念であり、あらゆる状況でホイールを再発明し、高品質のコードを提供することなく、同期プログラミングを非同期コードに変換できます。 Deferredは、この結果が不明な場合の関数の戻り結果です(受信されていない、別のスレッドで受信されるなど)。Deferredでできることは何ですか? 結果を受け取ったときに呼び出されるハンドラーのチェーンに自分自身を掛けることができます。 同時に、Deferredは実行の肯定的な結果だけでなく、関数またはハンドラーによって生成された例外も保持でき、例外の処理、再スローなどが可能です。 実際、同期コードの場合、Deferredに関して多かれ少なかれ明白な並列性があります。 クロージャーなどのプログラミング言語機能であるDeferredを使用した効果的な開発には、ラムダ関数が役立ちます。



同期コードと、遅延の観点からの代替の例を次に示します。



try: #   HTTP   page = downloadPage(url) #   print page except HTTPError, e: #   print "An error occured: %s", e
      
      





Deferredを使用した非同期バージョンでは、次のように記述されます。



 def printContents(contents): """ Callback,     ,   . """ print contents def handleError(failure): """ Errback ( ),    . """ #     HTTPError,   # "" . failure.trap(HTTPError) #    print "An error occured: %s", failure #        #    Deferred deferred = downloadPage(url) #   Deferred-    #   (callback, errback). deferred.addCallback(printContents) deferred.addErrback(handleError)
      
      





実際には、通常、作業中にDeferredが受け取る関数からDeferredを返し、多数のハンドラーをアタッチし、例外を処理し、Deferredを通じていくつかの例外を返します(スローします)。 より複雑な例として、 memcachedのデータ構造に関する記事のアトミックカウンターの例の非同期バージョンのコードを示します。ここでは、ネットワークサービスとしてのmemcachedへのアクセスはDeferred、つまり MemcacheクラスメソッドはDeferredを返します(操作の結果またはエラーのいずれかを返します)。



 class MCCounter(MemcacheObject): def __init__(self, mc, name): """ . @param name:   @type name: C{str} """ super(MCCounter, self).__init__(mc) self.key = 'counter' + name def increment(self, value=1): """      . @param value:  @type value: C{int} @return: Deferred,   """ def tryAdd(failure): #   KeyError,   "" #  failure.trap(KeyError) #   ,      d = self.mc.add(self.key, value, 0) #   -     , #    d.addErrback(tryIncr) #  Deferred,  ""   # Deferred,      return d def tryIncr(failure): #    tryAdd failure.trap(KeyError) d = self.mc.incr(self.key, value) d.addErrback(tryAdd) return d #   ,  Deferred d = self.mc.incr(self.key, value) #   d.addErrback(tryAdd) #  Deferred  ,    : # ) ,     # )     (,  ) return d def value(self): """   . @return:    @rtype: C{int} @return: Deferred,   """ def handleKeyError(failure): #   KeyError failure.trap(KeyError) #   —  0,    #  Deferred return 0 #     d = self.mc.get(self.key) #      d.addErrback(handleKeyError) #  Deferred,      #   callback      return d
      
      





上記のコードは、一般的に使用される操作を組み合わせることにより、「より短く」書くことができます。例えば:



 return self.mc.get(self.key).addErrback(handleKeyError)
      
      





ほぼすべての同期コード設計で、Deferredを使用して非同期コンセプトでアナログを見つけることができます。



多くの場合、スレッドは非同期プログラムで使用され、計算手順を実行し、入出力のブロッキングを実装します(非同期アナログが存在しない場合)。 これはすべて単純な「ワーカー」モデルで簡単にモデル化され、有能なアーキテクチャと明示的に同期する必要はありませんが、Deferredを使用した計算の一般的なフローにはすべてがエレガントに含まれています。



 def doCalculation(a, b): """     ,   -,    . """ return a/b def printResult(result): print result def handleDivisionByZero(failure): failure.trap(ZeroDivisionError) print "Ooops! Division by zero!" deferToThread(doCalculation, 3, 2).addCallback(printResult).addCallback( lambda _: deferToThread(doCalculation, 3, 0).addErrback(handleDivisionByZero))
      
      





上記の例では、 deferToThread



関数は指定された関数の実行を別のスレッドに転送し、Deferredを返します。これにより、関数の結果が非同期に受信されるか、スローされた場合は例外が返されます。 最初の除算(3/2)は別のスレッドで実行され、その結果が画面に出力され、次に別の計算(3/0)が起動され、 handleDivisionByZero



関数によって処理される例外がスローされます。



ある記事では、Deferredについて言いたいことの一部を説明することはできませんが、それらがどのように機能するかについては何とか書きませんでした。 興味を持ってもらうことができたら、以下の資料を読んでください。もっと書くことを約束します。



追加資料






All Articles