Twisted による非同期プログラミング

Twisted Documentation の日本語訳

  1. 並行プログラミング (concurrent programming) の紹介
  2. Deferred
  3. Deferred が解決する問題
  4. Deferreds - まだ到着していないデータの通知
  5. まとめ

この文書は非同期プログラミングというプログラミング・モデルの紹介と Twisted の Deferred について解説したものです。「イベントの確約された結果」をシンボル化し、その内容をハンドラ関数に渡せるよう抽象化されたものが Deferred です。

この文書は Twisted 初心者向けのものですが、読者は Python のプログラミングに慣れ親しんでおり、サーバやクライアント、ソケットなどネットワークの基礎的な知識があることを前提に書かれています。これを読むことにより、(タスクのインターリーブを実行する)並行プログラミングの概要および Twisted の並行処理手法であるノンブロッキング・コード (non-blocking code)非同期コード (asynchronous code) について理解できることでしょう。

Deferred を含む並行モデルの解説の後に、Deferred オブジェクトが返す結果をハンドリングする方法について解説します。

並行プログラミング (concurrent programming) の紹介

コンピュータタスクにはその完了まで時間がかかるケースが多くあります。そしてその原因は次の二つに分類できます。

  1. (たとえば大きな数の階乗計算など)コンピュータ的に重い処理であり、答を計算するために一定の CPU 時間を要する場合。
  2. 重い処理ではないが、答えを出すために必要なデータが届くの待たなければならない場合

応答を待つ

データの到着を待たなければならないというのは、ネットワーク・プログラミングの基本的な性質です。なんらかの情報をまとめてメールで送信する機能を考えてみましょう。この機能を実現するには、まずリモートのサーバに接続しなければなりません。そしてそのサーバからの応答を待ちます。次にリモートのサーバに対しメールを送信可能かどうか問い合わせて、その応答を待ちます。そしてメールを送信、その応答を待って最後に接続を切ります。

上記、いずれのステップも一定の時間を要します。データを送信し、その応答を待つという処理を一番単純なプログラミング・モデルで実現した場合、きわめて明白な制限が課せられることになります。それは、一度に数多くのメールを送信することができないということです。またメールを送信している間、このプログラムはほかの処理を一切できないことにもなります。

したがって、ネットワーク・プログラムは一部単純なものを除き、このようなプログラミング・モデルを採用していません。あるタスクが処理を継続するに当たり必要な何かを待ち続けている間も、他のタスクを実行可能なモデルは色々あるので、そのうちの何れかを選ぶことになります。

データを待たない

ネットワーク・プログラムを書く方法はたくさんありますが、その主な内容は次の通りです。

  1. 各ネットワーク接続毎に OS のプロセスを分けて処理する。これによって、あるプロセスが何かを待っている状態になっていても OS は他のプロセスの処理を進めることができます。
  2. 各ネットワーク接続毎にスレッドを分けて処理する。1スレッドを分けることによって、あるスレッドが何かを待っていても、他のスレッドが処理を進められるようになります。
  3. ノンブロッキング・システムコールを使い、すべての接続を一つのスレッドで処理する。

ノンブロッキング・コール

Twisted フレームワークで通常使われるのは三番目のノンブロッキング・コールです。

一つのスレッドで多くの接続を処理する場合、そのスケジューリングは OS ではなく、アプリケーションが責任を持っておこなわなければなりません。通常は、接続確立し、読み込みまたは書き込みの準備ができたときに、予め登録しておいた関数が呼び出されるように実装します。このようなプログラミング・モデルは非同期プログラミングイベントドリブン・プログラミング、あるいはコールバックベース・プログラミングなどと呼ばれています。

このモデルを用いて先ほどのメール送信機能を実現すると、次のようになります。

  1. まずリモートサーバへの接続を実行する関数を呼び出します
  2. 接続用関数は(接続確立を待たず)即座にリターンされます。接続が確立したときには、メール送信関数を呼び出すよう通知するようになります。
  3. 接続が確立すると、接続のメカニズムがメール送信関数に対し準備ができた旨の通知をします

前に紹介した途中でブロックしてしまう流れに対し、この手法を使った場合の利点はなんでしょう?前のプログラムだと接続が確立されるまでメール送信の次のパートが実行されないため、次のメールの送信処理開始などほかの処理を実行できませんが、このプログラムは接続確立を一々待たなくて済むのです。

コールバック

非同期プログラミングにおいて、データの準備ができたことをアプリケーションに通知する典型的な方法がコールバックと呼ばれるものです。アプリケーションはまず何らかのデータを要求する関数を呼び出します。この呼び出し過程で、データの準備ができたときにそのデータを引数として実行するコールバック関数を同時に渡します。コールバック関数はそのデータを使って、アプリケーションが必要とする処理を実行するようになっています。

関数がデータを要求し、データが返ってくるのを待ってそれを処理するのが同期プログラミングですが、非同期プログラミングの場合、関数はデータを要求し、データの準備ができたらライブラリがコールバックを呼び出すようにするだけで良いのです。

Deferred

Twisted はコールバックの流れを制御するために Deferred オブジェクトを使用します。クライアントアプリケーションは、非同期リクエストに対する結果が返ってきたときに呼び出す一連の関数を Deferred に登録します(これらがコールバック群であり、コールバックチェーンとも呼ばれます)。また同時に非同期リクエスト実行に失敗したとき呼び出す関数も登録できます(エラーバック群であり、エラーバックチェーンとも呼ばれます)。結果が返ってきたら、非同期ライブラリのコードは最初のコールバック(エラーが発生したときは最初のエラーバック)を呼び出します。Deferred オブジェクトはコールバック(またはエラーバック)関数のチェーンを辿り、コールバックの実行結果を次のコールバック関数に渡していきます。

Deferred が解決する問題

Deferred は冒頭に挙げた2番目の問題 — 多くの CPU 時間を必要とするわけではないのに、結果が返ってくるまで待たされる処理 — に対する解決策を提供します。ハードディスクへのアクセス応答待ち、データベースへの問合せ応答待ち、ネットワークの応答待ちなど、時間差の生じる処理などがこれに該当します。

Deferred は Twisted プログラムがデータを待つ一方で、停止状態に陥ることがないように設計されています。これはライブラリやアプリケーションに対し、シンプルなコールバック管理インターフェースを提供することで実現されています。ライブラリ側が結果を返すときは Deferred.callback、エラーを返すときは Deferred.errback を呼び出せば良いようになっています。アプリケーション側は、結果を処理するコールバックやエラーバックを呼び出してもらいたい順で Deferred に登録するだけで良いのです。

Deferred や類似のソリューションの背後にある基本的な考え方は、CPU をできる限りアクティブな状態に保つということです。データ待ちのタスクがある場合、CPU (そしてプログラム!)をアイドル状態("ブロック状態")にしないので、その間に他の処理が可能になります。処理すべきデータの準備ができたときは、そのプロセスがリターンする前にそのことが分かるようになっているのです。

Twisted で関数が Deferred を返す場合、呼び出した側の関数に通知を送ります。プログラムは、データが利用可能になったときに Deferred のコールバックを有効にして、データを処理するのです。

Deferreds - まだ到着していないデータの通知

先に述べたメール送信の例では、親関数がリモートサーバに接続する関数を呼び出していました。非同期プログラミングにおいて、接続関数は親関数が別の処理をできるように結果を待たなくていいという内容をリターンする必要があります。では親関数や制御プログラムはどのようにして接続がまだ確立していないことを知るのでしょう?あるいはどのようにして接続が確立したことを知るのでしょう?

Twisted にはこれらの状態を通知するオブジェクトがあります。接続関数はリターンする際、処理がまだ完了していないこと通知するために twisted.internet.defer.Deferred オブジェクトを返します。

Deferred は2つの役割を担っています。ひとつは"要求された処理はまだ完了していない"という通知としての役割です。また Deferred は、データが到着したとき実行すべき内容を登録できるようになっています。

コールバック

データが届いたときが何をすべきか Dererred に知らせるのがコールバックの追加 — データが到着したときに特定の関数を呼び出すよう Deferred に依頼すること — です。

Twisted ライブラリの Deferred を返す関数のひとつに twisted.web.client.getPage があります。次の例では getPage を呼び出しており、返ってきた Deferred に、データが到着したとき呼び出す、ページのコンテンツを処理するコールバックを追加しています。

from twisted.web.client import getPage

from twisted.internet import reactor

def printContents(contents):
    '''
    これが'コールバック'関数です。Deferred に追加され、データが到着した
    ときに Deferred から呼び出されます。
    '''

    print "Deferred が以下のコンテンツとともに printContents を呼び出しました。"
    print contents

    # Twisted のイベントハンドリングシステムを停止する。-- 通常これは上
    # 位でハンドリングされます。
    reactor.stop()

# getPage 呼び出すと即座に Deferred が返されます。同時にコンテンツのダ
# ウンロードが完了したら、そのコンテンツを引数にしてコールバックを呼び
# 出すことが確約されます。
deferred = getPage('http://twistedmatrix.com/')

# Deferred へのコールバック追加 -- ページコンテンツのダウンロードが完了
# したらprintContents 実行するように要求しています
deferred.addCallback(printContents)

# プロセスを開始する Twisted イベンドハンドリングシステムの開始 -- 通常、
# このようには書きません
reactor.run()

Deferred を使う場合、通常は二つのコールバックを登録します。最初のコールバックの結果が次のコールバックに引数として渡されます

from twisted.web.client import getPage

from twisted.internet import reactor

def lowerCaseContents(contents):
    '''
    これが'コールバック'関数です。Deferred に追加され、データが到着した
    ときに Deferred から呼び出されます。全ての文字を小文字に変換します。
    '''

    return contents.lower()

def printContents(contents):

    ''' これもコールバック関数です。lowerCaseContents の次に Deferred
    へ追加され、Deferred から lowerCaseContents の実行結果を引数にして
    呼び出されます。
    '''

    print contents
    reactor.stop()

deferred = getPage('http://twistedmatrix.com/')

# 二つのコールバックを Deferred に追加します -- ページコンテンツのダウ
# ンロードが完了したら lowerCaseContents を実行し、lowerCaseContents の
# 実行結果を引数にして printContents を実行するようになっています。
deferred.addCallback(lowerCaseContents)
deferred.addCallback(printContents)

reactor.run()

エラーハンドリング: エラーバック

非同期関数が結果を得る前にリターンするのと同様、エラーを検知する前にリターンすることもあります。接続の失敗、不正なデータ、プロトコルエラー等が考えられます。期待したデータが得られたときに呼び出すコールバックを Deferred に登録するのと同じやり方で、エラーが発生し、データを得られなかったときに呼び出すエラーハンドラ('エラーバック')を登録することができます。

from twisted.web.client import getPage

from twisted.internet import reactor

def errorHandler(error):

    ''' 
    これは'エラーバック'関数です。Dferred に追加されエラーイベントが発
    生したときに呼び出されます。
    '''

    # エラー内容を表示するだけで、あまり意味のあるエラーハンドリングで
    # はありません。
    print "An error has occurred: <%s>" % str(error)
    # プロセスを停止します。
    reactor.stop()

def printContents(contents):
    '''

    これは'コールバック'関数です。Deferred に登録され、ページコンテンツ
    を引数にして呼び出されます。
    '''

    print contents
    reactor.stop()

# エラーチェーンのデモとして実在しないページへのリクエストを発行し
# ています。
deferred = getPage('http://twistedmatrix.com/does-not-exist')

# ページコンテンツのハンドリングを行うコールバックを Deferred に登録
deferred.addCallback(printContents)

# エラーのハンドリングを行うエラーバックを Deferred に登録
deferred.addErrback(errorHandler)

reactor.run()

まとめ

このドキュメントで解説した内容は以下の通りです。

  1. 重要なネットワークプログラムにおいて並行処理が必要とされるのはなぜか
  2. Twisted フレームワークは非同期呼び出しによって並行処理をサポートしている
  3. Twisted フレームワークはコールバックチェーンを管理する Deferred オブジェクトを備えている
  4. Deferred オブジェクトを返す関数の例 getPage
  5. Deferred へコールバックやエラーバックを登録する方法
  6. Deferred のコールバックチェーンおよびエラーバックチェーンがどのように実行されるか

関連文書

抽象モデル Deferred は Twisted プログラミングの中心となるものです。もっと詳しく解説した文書がいくつかあります。

  1. Deferred の使い方Deferred および Deferred チェーンの使い方の詳しい解説です
  2. Deferred の作り方Deferred の作成方法とコールバックチェーン実行についての解説です

Footnotes

  1. この方法にはいくつかのバリエーションがありますが、特定サイズのスレッド・プールを用意して、各接続毎にスレッドを割り当てるというのが典型的な例です。