Django におけるスレッド

Threading in Django | Artful Code

本稿は上記リンク元の和訳になります
転載ミス、誤訳等については適宜修正します

web 開発で利便性の高いスレッドの使用方法は珍しく、特に Python のグローバルインタープリタロックと共に使用するのは稀です。これに対して、幾つか注目すべき例外があります。

グローバルインタープリタロック(GIL) は、全ての Python オブジェクトがスレッドセーフではないとき、内部的なスレッドセーフを維持するために使用するメソッドです。GIL は、Python オブジェクトが同時に複数のスレッドによって変更されないことを保証します。これは Python プログラマに対して透過的で、GIL は内部構造をロックしています。明示的にロックされずに複数スレッドによって変更されるリストは、不規則的な動作もするでしょう。

これは web 開発におけるスレッドの利便性を低くします。というのは、Python は直接的にリクエスト間(間接的に、クライアント経由で、状態はセッションやクッキー、又は似たようなものを使用して維持されるかもしれません)の状態を維持できません。各々のリクエストは新しいスレッドです:

  1. 何のデータがリクエストされたかを決定する
  2. データを取得する
  3. データをフォーマットする
  4. データを送信する


これらの行程の1つ1つは、その前の行程からの状態情報に依存します。非同期処理では多くの箱はありません。スレッドの利便性の低さは web サーバへ送り返されることです。Ocsigen のように、それはサーバ/フレームワークで解決されることで、利点も持っています。

多くの web アプリケーションでは、しかしながら、単純なデータベースのフロントエンドではありません。スレッドの利便性が高くなる共通状況があります; 具体的に言うと、リクエストの副作用があるときです。

ちなみに、専門家は、POST リクエストのみが副作用を持つとあなたに伝えるでしょう。これは語弊があります。POST は、ユーザが意図した副作用のために使用されるべきです。しかしながら、ページの読み込み時にエラーがあるときは何が起きているのだろうか?

エラーメッセージ

コーディング又は環境エラーのイベントで、Django は私にメールを送信します。これは素晴らしい(そして、時々迷惑な)機能です。しかし、その問題が、そのコードというよりむしろ、私たちが入力したデータによって引き起こされたとしたらどうだろうか?私は、コードの中でこれを検出し、そのユーザに迷惑をかけたことを謝罪する素敵なエラーページを表示します。しかし、今は、私は何らかの方法でデータベースのあるエントリが不正であるという内容をチームへ知らせたいです。

メッセージを送信することは、特に Django がリモートメールサーバへ接続しているならば、しばらく時間がかかります。これはスレッドの良い使用方法です。そのスレッドは、副作用どころか集中して、できるだけ小さなデータ処理を扱うべきです。

import threading
from django.core.mail import send_mail
 
def foo(request, identifier):
    data = get_some_data(identifier)
    try:
        error_check(data) # もしデータが不正なら例外を発生させる
    except InvalidData, e:
        # メインスレッドのここで私たちの情報をフォーマットする
        subject = "Invalid data in entry %d" % identifier
        message = """
            There is an error in entry %d.  Please check this
            data at http://path/to/django/admin/app/table/%d.
        """ % (identifier, identifier)
        recipients = ['someone@somewere.com', 'someoneelse@somewhere.com']
        from = 'root@server.com'
 
        # メッセージを送信するためにデーモンモードで新しいスレッドを作成する
        t = threading.Thread(target=send_mail,
                             args=[subject, message, from, recipients],
                             kwargs={'fail_silently': True})
        t.setDaemon(True)
        t.start()
    return HttpResponseServerError(some_error_page)

それを起動する前にスレッド上で t.setDaemon(True) を使用するのに注意してください。これは Python がクライアントへデータを返す前にスレッドの終了を待たないということを伝えています。

ファイルに書き込む

多くの web プログラムは静的なファイルへデータを書き込みます。アプリケーションによってはログを保持します。他には、静的な HTML ファイルをエンタープライズサーバへ公開します。GIL はファイル IO 操作中に解放されるので、Python スレッドの凄い使用方法があります。

リモートのデータをキャッシュする

私たちは大企業の、様々な(しばしば独立した)部署でアプリケーションを開発します。私は、しばしば、私のアプリケーションが他のシステムから展開された可変データに依存していて、しかし、そのデータのデータベースへ直接アクセスできない状況に遭遇します。

低階層の内部ネットワーク遅延や各々の読み込まれたページ上で展開されたデータをダウンロードするのに依存するよりも、私はそのデータを同期するためのスクリプトを使用しながら、ローカルコピーを保持してそのデータをキャッシュします。単純なデータでは、これは Django の低レベルキャッシュを使用しながら保存されます。しかし、そのデータを維持するために幾つかの状態が必要になる場合、データベーステーブルがより良い方法です。

多くのそのようなオブジェクトがあるとき、定期的な更新処理は大きな負荷になります。特にオブジェクトの数が規則的に増加するような場合です。それ故に、その解法は、そのデータが(ページ読み込みで)アクセスされたイベントをトリガーにすることです。

各々のキャッシュされたデータベースエントリは、その最後の更新を書き留めるためのタイムスタンプフィールドを取得します。そのモデルは更新メソッドを定義します。そして、そのモデルのマネージャの get() メソッドは、データが更新される必要があるかどうかを調べるために、そのエントリのタイムスタンプを調べます。更新処理は、別々のスレッドで呼び出されます。

警告: Django では、リレーションを経由してモデルインスタンスが見つかったとき、そのマネージャの get() メソッドは呼び出されません!これは、これらのオブジェクトはモデル自身のマネージャではなく django.db.models.fields.related.ManyRelatedManager 経由でアクセスされるからです。もし、コードを重複させることなくこれを解決したいならば、Django signals を調べてください。

その問題はこれは単純な副作用ではないということです、そして GIL は妨げになります。その解決方法は、メインスレッドで更新される必要があるオブジェクトがあるかどうかを決定するための必要な全てのロジックを実行することです。ちょうどユーザへレスポンスを返す前に、その更新スレッドはデーモンモードで起動されます。それは GIL の競合を最小限にします。そのユーザはこのページ上ではオブジェクトの更新されたバージョンを取得しないでしょう、しかし、次のユーザは取得します。

その他の代替方法は、(それ自身のインタープリタインスタンスとそれ自身の GIL と共に)別々のプロセスで更新処理を起動するために os.fork 又は subprocess モジュールを使用することです。この解決方法は、PHP では、スレッド不足のためによく使われています。Python では、スレッドはより使い易く、リソース不足も少なくなる傾向にあります。