web-dev-qa-db-ja.com

Python GObject Introspectionアプリで非同期タスクを実行する方法

Python + GObjectアプリを作成していますが、起動時にディスクから重要なデータを読み取る必要があります。データは同期的に読み取られ、読み取り操作の完了には約10秒かかります。その間、UIのロードが遅延します。

タスクを非同期に実行し、準備ができたらUIをブロックせずに通知を受け取りたいと思います。

def take_ages():
    read_a_huge_file_from_disk()

def on_finished_long_task():
    print "Finished!"

run_long_task(task=take_ages, callback=on_finished_long_task)
load_the_UI_without_blocking_on_long_task()

私は過去にこの種のことのために GTask を使用しましたが、そのコードが3年も触れられておらず、GObject Introspectionに移植されていないことを心配しています。最も重要なことは、Ubuntu 12.04では使用できなくなったことです。そこで、標準のPython方法またはGObject/GTK +の標準方法のいずれかで、タスクを非同期で実行する簡単な方法を探しています。

編集:ここに私がやろうとしていることの例のコードがあります。コメントで提案されているようにpython-deferを試しましたが、長いタスクを非同期で実行し、完了するのを待たずにUIをロードすることができませんでした。 テストコードを参照

非同期タスクを実行し、終了時に通知を受ける簡単で広く使用されている方法はありますか?

16
David Planella

あなたの問題は非常に一般的なものであるため、多くの解決策があります(小屋、マルチプロセッシングまたはスレッド処理を備えたキュー、ワーカープールなど)

非常に一般的であるため、pythonビルドインソリューションもあります(3.2ではバックポートされています: http://pypi.python.org/pypi/futures ) concurrent.futuresと呼ばれます。 「未来」は多くの言語で利用できるため、pythonはそれらを同じと呼びます。一般的な呼び出しを次に示します(そして、ここに 完全な例 がありますが、db部分はsleepに置き換えられます。以下の理由を参照)。

from concurrent import futures
executor = futures.ProcessPoolExecutor(max_workers=1)
#executor = futures.ThreadPoolExecutor(max_workers=1)
future = executor.submit(slow_load)
future.add_done_callback(self.on_complete)

さて、単純な例が示すよりもはるかに複雑な問題について考えてみましょう。一般に、これを解決するスレッドまたはプロセスがありますが、例が非常に複雑な理由は次のとおりです。

  1. ほとんどのPython実装にはGILがあり、スレッドがマルチコアを完全に利用するnotにします。だから:Pythonでスレッドを使用しないでください!
  2. DBからslow_loadで返すオブジェクトは選択可能ではありません。つまり、プロセス間で単純に渡すことはできません。だから:softwarecenterでのマルチプロセッシングの結果はありません!
  3. 呼び出すライブラリ(softwarecenter.db)はスレッドセーフではないため(gtkなどを含むようです)、スレッドでこれらのメソッドを呼び出すと、奇妙な動作が発生します(私のテストでは、「コアダンプ」から「コアダンプ」まで、すべてがシンプルになります)結果なしで終了)。そのため、softwarecenterにはスレッドがありません。
  4. Gtkのすべての非同期コールバックは、glibメインループで呼び出されるコールバックをスケジュールすることを除いて、anythingを行うべきではありません。コールバックの追加を除いて、print、gtk状態の変更はありません!
  5. Gtkなどは、すぐに使用できるスレッドでは機能しません。 threads_initを実行する必要があり、gtkまたは同様のメソッドを呼び出す場合、そのメソッドを保護する必要があります(以前のバージョンではgtk.gdk.threads_enter()gtk.gdk.threads_leave()でした。たとえばgstreamerを参照してください。 http://pygstdocs.berlios.de/pygst-tutorial/playbin.html )。

私はあなたに次の提案をすることができます:

  1. slow_loadを書き換えて、選択可能な結果を​​返し、プロセスでフューチャーを使用します。
  2. Softwarecenterからpython-aptなどに切り替えます(おそらく気に入らないでしょう)。しかし、Canonicalに雇われているので、ソフトウェアセンターの開発者に直接ドキュメントをソフトウェアに追加する(たとえば、スレッドセーフではないことを伝える)ように依頼することもできますソフトウェアセンターをスレッドセーフにする。

注:他のソリューション(Gio.io_scheduler_Push_jobasync_calldoが提供するソリューションはtime.sleepで動作しますが、 softwarecenter.dbではありません。これは、すべてgtkおよびsoftwarecenterで動作しないスレッドまたはプロセスとスレッドに要約されるためです。

15
xubuntix

GIOのI/Oスケジューラを使用する別のオプションを次に示します(以前Pythonで使用したことはありませんが、以下の例は問題なく実行されるようです)。

from gi.repository import GLib, Gio, GObject
import time

def slow_stuff(job, cancellable, user_data):
    print "Slow!"
    for i in xrange(5):
        print "doing slow stuff..."
        time.sleep(0.5)
    print "finished doing slow stuff!"
    return False # job completed

def main():
    GObject.threads_init()
    print "Starting..."
    Gio.io_scheduler_Push_job(slow_stuff, None, GLib.PRIORITY_DEFAULT, None)
    print "It's running async..."
    GLib.idle_add(ui_stuff)
    GLib.MainLoop().run()

def ui_stuff():
    print "This is the UI doing stuff..."
    time.sleep(1)
    return True

if __== '__main__':
    main()
11

内省されたGio APIを使用して、非同期メソッドでファイルを読み取り、最初の呼び出しを行うときに、GLib.timeout_add_seconds(3, call_the_gio_stuff)でタイムアウトとして実行します。ここで、call_the_gio_stuffFalseを返す関数です。

ここでタイムアウトを追加する必要があります(ただし、異なる秒数が必要になる場合があります)。これは、Gio非同期呼び出しは非同期ですが、非ブロッキングではないため、大きなファイルを読み込む、または大きなディスクUIとI/Oはまだ同じ(メイン)スレッドにあるため、ファイル数が多いとUIがブロックされる可能性があります。

PythonのファイルI/O APIを使用して、非同期でメインループと統合する独自の関数を作成する場合は、GObjectとしてコードを記述するか、コールバックを渡すか、python-deferを使用して支援する必要があります。あなたはそれを行う。ただし、ここでGioを使用することをお勧めします。これは、特にUXでファイルを開いたり保存したりする場合に、多くのNice機能を提供できるためです。

2
dobey

GLib Mainloopが優先度の高いイベントをすべて終了すると、GLib.idle_add(callback)を使用して長時間実行タスクを呼び出すこともできます(UIの構築を含むと信じています)。

2
mhall119

これは@mhallが提案したことを行うための複雑な方法であることに注意する必要があると思います。

基本的に、これを実行してから、async_callの関数を実行します。

動作を確認したい場合は、スリープタイマーで遊んで、ボタンをクリックし続けることができます。サンプルコードがある以外は、@ mhallの答えと本質的に同じです。

これに基づいて これは私の仕事ではありません。

import threading
import time
from gi.repository import Gtk, GObject



# calls f on another thread
def async_call(f, on_done):
    if not on_done:
        on_done = lambda r, e: None

    def do_call():
        result = None
        error = None

        try:
            result = f()
        except Exception, err:
            error = err

        GObject.idle_add(lambda: on_done(result, error))
    thread = threading.Thread(target = do_call)
    thread.start()

class SlowLoad(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title="Hello World")
        GObject.threads_init()        

        self.connect("delete-event", Gtk.main_quit)

        self.button = Gtk.Button(label="Click Here")
        self.button.connect("clicked", self.on_button_clicked)
        self.add(self.button)

        self.file_contents = 'Slow load pending'

        async_call(self.slow_load, self.slow_complete)

    def on_button_clicked(self, widget):
        print self.file_contents

    def slow_complete(self, results, errors):
        '''
        '''
        self.file_contents = results
        self.button.set_label(self.file_contents)
        self.button.show_all()

    def slow_load(self):
        '''
        '''
        time.sleep(5)
        self.file_contents = "Slow load in progress..."
        time.sleep(5)
        return 'Slow load complete'



if __== '__main__':
    win = SlowLoad()
    win.show_all()
    #time.sleep(10)
    Gtk.main()

追加の注意として、適切に終了する前に他のスレッドを終了させるか、子スレッドでfile.lockを確認する必要があります。

コメントに対処するために編集:
最初はGObject.threads_init()を忘れていました。明らかに、ボタンが発動すると、スレッドが初期化されました。これは私にとって間違いを隠してくれました。

通常、フローはメモリ内にウィンドウを作成し、スレッドがボタンを更新するのを完了するとすぐに他のスレッドを起動します。 Gtk.mainを呼び出す前にスリープを追加して、ウィンドウが描画される前に完全な更新が実行されることを確認しました。また、スレッドの起動がウィンドウの描画をまったく妨げないことを確認するためにコメントアウトしました。

1
RobotHumans