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


16

私は、起動時にディスクから重要でないデータを読み取る必要がある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を使用しましたが、GObject Introspectionに移植されたことはもちろん、そのコードが3年も触れられていないことを心配しています。最も重要なことは、Ubuntu 12.04では使用できなくなったことです。そこで、標準のPythonの方法またはGObject / GTK +の標準の方法で、非同期でタスクを実行する簡単な方法を探しています。

編集:ここに私がやろうとしていることの例といくつかのコードがあります。python-deferコメントで示唆されているように試しましたが、長いタスクを非同期で実行し、UIの読み込みを完了するのを待たずにロードすることはできませんでした。テストコードを閲覧します

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


きれいな例ではありませんが、これがあなたが探しているものだと確信しています:raw.github.com/gist/1132418/…– RobotHumans
12:52

クール、私はあなたのasync_call機能が私が必要なものかもしれないと思う。私はそれを受け入れて、テスト後にあなたに信用できるように、少し拡張して答えを追加してくれませんか?ありがとう!
デビッドプラネラ

1
とても便利な質問です。;-)
ラファウCieślak

回答:


15

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

それは非常に一般的であるため、concurrent.futuresと呼ばれるPythonビルドインソリューション(3.2で、ただしバックポート:http : //pypi.python.org/pypi/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があり、スレッドがマルチコアを完全に利用しないようにします。だから:Pythonでスレッドを使用しないでください!
  2. slow_loadDBから返すオブジェクトは選択可能ではありません。つまり、プロセス間で単純に渡すことはできません。だから:softwarecenterでのマルチプロセッシングの結果はありません!
  3. 呼び出すライブラリ(softwarecenter.db)はスレッドセーフではないため(gtkなどを含むようです)、スレッドでこれらのメソッドを呼び出すと、奇妙な動作が発生します(私のテストでは、「コアダンプ」から「コアダンプ」まで、すべてがシンプルになります)結果なしで終了)。そのため、softwarecenterにはスレッドがありません。
  4. gtkのすべての非同期コールバックは、glibメインループで呼び出されるコールバックをスケジュールする以外は何もしません。そのため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_call提供するソリューションはで動作しますtime.sleepが、では動作しませんsoftwarecenter.db。これは、すべてgtkおよびで動作しないスレッドまたはプロセスとスレッドに要約されるためsoftwarecenterです。


ありがとう!なぜそれができないのかを非常に詳細に示しているので、あなたの答えを受け入れます。残念ながら、私は(かかわらず、それは、素量のためである私のアプリでのUbuntu 12.04のためにパッケージ化されていないソフトウェアを使用することはできませんlaunchpad.net/ubuntu/+source/python-concurrent.futures私はできないとこだわっていると思うので、)タスクを非同期に実行します。Software Centerの開発者に話をノートについては、私はコードとドキュメントへの変更を貢献するべきボランティアと同じ位置に、またはそれらに話をするよ:-)
デヴィッドPlanella

GILはIO中にリリースされるため、スレッドを使用してもまったく問題ありません。ただし、非同期IOを使用する場合は必要ありません。
jfs

10

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 __name__ == '__main__':
    main()

slow_stuffが終了したら、メインスレッドで何かを実行する場合は、GIO.io_scheduler_job_send_to_mainloop()も参照してください。
ジーク

答えと例に感謝します。残念ながら、現在のタスクではGio APIを使用して非同期で実行する機会はないようです。
デビッドプラネラ

これは本当に役に立ちましたが、私が知る限り、Gio.io_scheduler_job_send_to_mainloopはPythonには存在しません:(
sil

2

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


マイクありがとう。はい、それは間違いなくUIの準備ができたときにタスクを開始するのに役立ちます。しかし、一方で、callbackが呼び出されると、それが同期的に行われ、UIがブロックされることを理解していますか?
デビッドプラネラ

idle_addはそのようには機能しません。idle_addでブロッキング呼び出しを行うことは依然として悪いことであり、UIの更新が行われないようにします。また、非同期APIでさえブロックすることができます。UIやその他のタスクのブロックを回避する唯一の方法は、バックグラウンドスレッドでブロックすることです。
-dobey

理想的には、遅いタスクをチャンクに分割して、アイドルコールバックでそのタスクの一部を実行し、戻る(およびUIコールバックなどのその他のものを実行させる)ことができ、コールバックが再度呼び出されたらさらに作業を続けることができます。オン。
ジークフリートゲヴァッター

落とし穴はidle_add、コールバックの戻り値が重要ということです。真の場合、再度呼び出されます。
フリム

2

イントロスペクトされたGioAPIを使用して、非同期メソッドでファイルを読み取り、最初の呼び出しを行うときに、GLib.timeout_add_seconds(3, call_the_gio_stuff)where call_the_gio_stuffが返される関数でタイムアウトとして実行しますFalse

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

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


ありがとう@dobey。私は実際にディスクから直接ファイルを読んでいるわけではありません。おそらく、元の投稿でそれをより明確にすべきでした。私が実行している長期実行タスクは、askubuntu.com / questions / 139032 / への回答に従ってSoftware Centerデータベースを読み取ることなので、GioAPI を使用できるかどうかはわかりません。私が不思議に思ったのは、GTaskを使用していたのと同じ方法で、一般的な長時間実行タスクを非同期に実行する方法があるかどうかです。
デビッドプラネラ

GTaskが正確に何なのかわかりませんが、gtask.sourceforge.netを意味するなら、それを使用すべきではないと思います。それが何か別のものである場合、それが何であるかはわかりません。しかし、私が述べた2番目のルートを採用し、非同期APIを実装してそのコードをラップするか、すべてをスレッドで実行する必要があるようです。
-dobey

質問にはそれへのリンクがあります。GTaskは(だった):chergert.github.com/gtask
デビッドプラネラ

1
ああ、それはpython-deferによって提供されるAPI(およびtwistedの遅延API)と非常によく似ています。おそらく、python-deferの使用を検討する必要がありますか?
-dobey

1
たとえば、GLib.idle_add()を使用して、メインの優先度イベントが発生するまで、呼び出されるのを遅らせる必要があります。:このようなpastebin.ubuntu.com/1011660
dobey

1

これは@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 __name__ == '__main__':
    win = SlowLoad()
    win.show_all()
    #time.sleep(10)
    Gtk.main()

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

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

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


1
ありがとう。私はそれに従うことができるかどうかわかりません。たとえばslow_load、UIの起動後すぐに実行されることを期待していましたが、ボタンがクリックされない限り、呼び出されることはないようです。タスクの状態の。
デビッドプラネラ

申し訳ありませんが、私は1行を逃しました。それでした。GObjectにスレッドの準備をするように指示するのを忘れました。
RobotHumans

ただし、スレッドからメインループを呼び出しているため、問題が発生する可能性がありますが、実際の作業を行わない些細な例では簡単に公開されない場合があります。
-dobey

有効なポイントですが、
DBusを

うーん、async_callこの例で実行するとうまくいきますが、アプリに移植したときに混乱が生じ、実際のslow_load機能を追加します。
デビッドプラネラ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.