Cara menjalankan tugas asinkron di aplikasi Introspeksi Python GObject

16

Saya sedang menulis aplikasi Python + GObject yang perlu membaca sejumlah data non-sepele dari disk saat mulai. Data dibaca secara serempak dan dibutuhkan sekitar 10 detik untuk menyelesaikan operasi baca, selama itu waktu pemuatan UI tertunda.

Saya ingin menjalankan tugas secara tidak sinkron, dan mendapatkan notifikasi ketika sudah siap, tanpa memblokir UI, kurang lebih seperti:

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()

Saya telah menggunakan GTask di masa lalu untuk hal semacam ini, tetapi saya khawatir bahwa kodenya belum tersentuh dalam 3 tahun, apalagi telah diporting ke GObject Introspection. Yang terpenting, ini tidak lagi tersedia di Ubuntu 12.04. Jadi saya mencari cara mudah untuk menjalankan tugas secara tidak sinkron, baik dengan cara Python standar atau dengan cara standar GObject / GTK +.

Sunting: ini beberapa kode dengan contoh apa yang saya coba lakukan. Saya sudah mencoba python-deferseperti yang disarankan dalam komentar, tetapi saya tidak bisa mengelola untuk menjalankan tugas panjang secara tidak sinkron dan membiarkan UI memuat tanpa harus menunggu sampai selesai. Telusuri kode uji .

Apakah ada cara yang mudah dan banyak digunakan untuk menjalankan tugas yang tidak sinkron dan mendapatkan pemberitahuan setelah selesai?

David Planella
sumber
Ini bukan contoh yang bagus, tapi saya cukup yakin ini yang Anda cari: raw.github.com/gist/1132418/…
RobotHumans
Keren, saya pikir async_callfungsi Anda mungkin yang saya butuhkan. Maukah Anda mengembangkannya sedikit dan menambahkan jawaban, sehingga saya dapat menerimanya dan menghargai Anda setelah saya mengujinya? Terima kasih!
David Planella
1
Pertanyaan bagus, sangat berguna! ;-)
Rafał Cieślak

Jawaban:

15

Masalah Anda adalah masalah yang sangat umum, oleh karena itu ada banyak solusi (gudang, antrian dengan multiprosesing atau threading, kolam pekerja, ...)

Karena sangat umum, ada juga solusi built-in python (dalam 3.2, tetapi didukung di sini: http://pypi.python.org/pypi/futures ) disebut concurrent.futures. 'Futures' tersedia dalam banyak bahasa, oleh karena itu python menyebutnya sama. Inilah beberapa panggilan umum (dan ini adalah contoh lengkap Anda , namun, bagian db digantikan oleh tidur, lihat alasannya di bawah).

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)

Sekarang untuk masalah Anda, yang jauh lebih rumit daripada contoh sederhana yang Anda sarankan. Secara umum Anda memiliki utas atau proses untuk menyelesaikan ini, tetapi inilah mengapa contoh Anda sangat rumit:

  1. Sebagian besar implementasi Python memiliki GIL, yang membuat utas tidak sepenuhnya menggunakan multicores. Jadi: jangan gunakan utas dengan python!
  2. Objek yang Anda ingin kembali slow_loaddari DB tidak dapat dipilih, yang berarti bahwa mereka tidak bisa begitu saja dilewatkan di antara proses. Jadi: jangan multiprocessing dengan hasil softwarecenter!
  3. Pustaka yang Anda panggil (softwarecenter.db) bukan threadsafe (tampaknya menyertakan gtk atau serupa), oleh karena itu memanggil metode-metode ini dalam sebuah thread menghasilkan perilaku yang aneh (dalam pengujian saya, semuanya dari 'berfungsi' di atas 'dump inti' menjadi sederhana berhenti tanpa hasil). Jadi: tidak ada utas dengan softwarecenter.
  4. Setiap panggilan balik asinkron di gtk tidak boleh melakukan apa pun kecuali menjadwalkan panggilan balik yang akan dipanggil di glib mainloop. Jadi: tidak print, tidak ada perubahan status gtk, kecuali menambahkan panggilan balik!
  5. GTK dan sama tidak bekerja dengan utas di luar kotak. Anda perlu melakukan threads_init, dan jika Anda memanggil gtk atau metode sama, Anda harus melindungi metode yang (di versi sebelumnya ini gtk.gdk.threads_enter(), gtk.gdk.threads_leave()lihat misalnya gstreamer:. Http://pygstdocs.berlios.de/pygst-tutorial/playbin. html ).

Saya dapat memberi Anda saran berikut:

  1. Tulis ulang Anda slow_loaduntuk mengembalikan hasil yang dapat dipilih dan menggunakan futures dengan proses.
  2. Beralih dari softwarecenter ke python-apt atau serupa (Anda mungkin tidak suka itu). Tetapi karena Anda dipekerjakan oleh Canonical, Anda dapat meminta pengembang softwarecenter secara langsung untuk menambahkan dokumentasi ke perangkat lunak mereka (mis. Menyatakan bahwa itu bukan thread aman) dan bahkan lebih baik lagi, membuat softwarecenter threadsafe aman.

Sebagai catatan: solusi yang diberikan oleh orang lain ( Gio.io_scheduler_push_job, async_call) melakukan pekerjaan dengan time.sleeptapi tidak dengan softwarecenter.db. Ini, karena semuanya bermuara pada utas atau proses dan utas untuk tidak bekerja dengan gtk dan softwarecenter.

xubuntix
sumber
Terima kasih! Saya akan menerima jawaban Anda karena itu menunjukkan saya dengan sangat rinci mengapa itu tidak bisa dilakukan. Sayangnya, saya tidak dapat menggunakan perangkat lunak yang tidak dikemas untuk Ubuntu 12.04 di aplikasi saya (itu untuk Quantal, meskipun launchpad.net/ubuntu/+source/python-concurrent.futures ), jadi saya kira saya terjebak dengan tidak dapat untuk menjalankan tugas saya secara tidak sinkron. Mengenai catatan untuk berbicara dengan pengembang Pusat Perangkat Lunak, saya berada di posisi yang sama dengan sukarelawan mana pun untuk menyumbangkan perubahan pada kode dan dokumentasi atau untuk berbicara dengan mereka :-)
David Planella
GIL dilepaskan selama IO sehingga sangat baik untuk menggunakan utas. Padahal itu tidak perlu jika async IO digunakan.
jfs
10

Berikut opsi lain menggunakan Penjadwal I / O GIO (Saya belum pernah menggunakannya sebelumnya dari Python, tetapi contoh di bawah ini tampaknya berjalan dengan baik).

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()
Siegfried Gevatter
sumber
Lihat juga GIO.io_scheduler_job_send_to_mainloop (), jika Anda ingin menjalankan sesuatu di utas utama setelah slow_stuff selesai.
Siegfried Gevatter
Terima kasih Sigfried atas jawaban dan contohnya. Sayangnya, tampaknya dengan tugas saya saat ini, saya tidak memiliki kesempatan untuk menggunakan API Gio untuk membuatnya berjalan secara tidak sinkron.
David Planella
Ini benar-benar berguna, tetapi sejauh yang saya tahu Gio.io_scheduler_job_send_to_mainloop tidak ada dalam Python :(
sil
2

Anda juga dapat menggunakan GLib.idle_add (callback) untuk memanggil tugas yang sudah berjalan lama setelah GLib Mainloop menyelesaikan semua itu acara dengan prioritas lebih tinggi (yang saya percaya termasuk membangun UI).

mhall119
sumber
Terima kasih Mike. Ya, itu pasti akan membantu memulai tugas ketika UI sudah siap. Tetapi di sisi lain, saya mengerti bahwa ketika callbackdipanggil, itu akan dilakukan secara serempak, sehingga memblokir UI, kan?
David Planella
Idle_add tidak berfungsi seperti itu. Membuat pemblokiran panggilan di idle_add masih merupakan hal yang buruk untuk dilakukan, dan itu akan mencegah pembaruan ke UI terjadi. Dan bahkan API yang tidak sinkron masih dapat memblokir, di mana satu-satunya cara untuk menghindari pemblokiran UI dan tugas lainnya, adalah dengan melakukannya di utas latar belakang.
dobey
Idealnya Anda akan membagi tugas lambat Anda menjadi potongan-potongan, sehingga Anda dapat menjalankannya sedikit dalam panggilan balik diam, kembali (dan biarkan hal-hal lain seperti UI panggil balik berjalan), terus melakukan beberapa pekerjaan lagi setelah panggilan balik dipanggil lagi, dan sebagainya di.
Siegfried Gevatter
Gotcha dengan idle_addadalah bahwa nilai kembali dari masalah panggilan balik. Jika itu benar, itu akan dipanggil lagi.
Flimm
2

Gunakan introspected GioAPI untuk membaca file, dengan metode asynchronous, dan ketika membuat panggilan awal, melakukannya sebagai batas waktu dengan GLib.timeout_add_seconds(3, call_the_gio_stuff)mana call_the_gio_stuffadalah fungsi yang kembali False.

Timeout di sini perlu ditambahkan (meskipun, jumlah detik yang berbeda mungkin diperlukan), karena sementara panggilan Gio async bersifat asinkron, mereka tidak non-blocking, yang berarti bahwa aktivitas hard disk yang berat membaca file besar, atau besar jumlah file, dapat mengakibatkan UI diblokir, karena UI dan I / O masih dalam utas (utama) yang sama.

Jika Anda ingin menulis fungsi Anda sendiri sebagai async, dan berintegrasi dengan loop utama, menggunakan file I / O API Python, Anda harus menulis kode sebagai GObject, atau meneruskan panggilan balik, atau menggunakan python-deferuntuk membantu Anda lakukan. Tapi yang terbaik adalah menggunakan Gio di sini, karena dapat membawa Anda banyak fitur bagus, terutama jika Anda melakukan membuka / menyimpan file di UX.

dobey
sumber
Terima kasih @obey. Saya tidak benar-benar membaca file dari disk secara langsung, saya mungkin harus membuatnya lebih jelas dalam posting asli. Tugas jangka panjang yang saya jalankan adalah membaca basis data Pusat Perangkat Lunak sesuai jawaban untuk askubuntu.com/questions/139032/… , jadi saya tidak yakin saya bisa menggunakan GioAPI. Apa yang saya pikirkan adalah apakah ada cara untuk menjalankan tugas jangka panjang yang umum secara asinkron dengan cara yang sama seperti yang dilakukan GTask.
David Planella
Saya tidak tahu apa sebenarnya GTask, tetapi jika Anda maksud gtask.sourceforge.net maka saya tidak berpikir Anda harus menggunakan itu. Jika itu sesuatu yang lain, maka saya tidak tahu apa itu. Tapi sepertinya Anda harus mengambil rute kedua yang saya sebutkan, dan menerapkan beberapa asynchronous API untuk membungkus kode itu, atau hanya melakukan semuanya dalam utas.
dobey
Ada tautan untuk itu dalam pertanyaan. GTask adalah: was
David Planella
1
Ah, itu terlihat sangat mirip dengan API yang disediakan oleh python-defer (dan twisted's deferred API). Mungkin Anda harus melihat menggunakan python-defer?
dobey
1
Anda masih perlu menunda panggilan itu, sampai setelah peristiwa prioritas utama terjadi, dengan menggunakan GLib.idle_add () misalnya. Seperti ini: pastebin.ubuntu.com/1011660
dobey
1

Saya pikir perlu dicatat bahwa ini adalah cara berbelit-belit untuk melakukan apa yang disarankan @mhall.

Intinya, Anda menjalankan ini lalu menjalankan fungsi async_call.

Jika Anda ingin melihat cara kerjanya, Anda dapat bermain dengan pengatur waktu tidur dan terus mengklik tombol. Ini pada dasarnya sama dengan jawaban @ mhall kecuali bahwa ada kode contoh.

Berdasarkan ini yang bukan pekerjaan saya.

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()

Catatan tambahan, Anda harus membiarkan utas lainnya selesai sebelum akan berakhir dengan benar atau memeriksa file. Buka utas anak Anda.

Edit ke alamat komentar:
Awalnya saya lupa GObject.threads_init(). Terbukti ketika tombol menyala, itu menginisialisasi threading untuk saya. Ini menutupi kesalahan saya.

Umumnya aliran membuat jendela dalam memori, segera luncurkan utas lainnya, saat utas selesai perbarui tombol. Saya menambahkan tidur tambahan bahkan sebelum saya menelepon Gtk.main untuk memverifikasi bahwa pembaruan lengkap BISA berjalan sebelum jendela bahkan ditarik. Saya juga berkomentar untuk memverifikasi bahwa peluncuran thread tidak menghalangi menggambar jendela sama sekali.

RobotHumans
sumber
1
Terima kasih. Saya tidak yakin saya bisa mengikutinya. Untuk satu, saya akan diharapkan slow_loadakan dieksekusi segera setelah UI dimulai, tetapi tampaknya tidak pernah dipanggil, kecuali tombol diklik, yang sedikit membingungkan saya, karena saya pikir tujuan tombol itu hanya untuk memberikan indikasi visual dari keadaan tugas.
David Planella
Maaf, saya melewatkan satu baris. Itu berhasil. Saya lupa memberi tahu GObject untuk bersiap-siap menerima utas.
RobotHumans
Tapi Anda menelepon ke loop utama dari utas, yang dapat menyebabkan masalah, meskipun mereka mungkin tidak mudah terungkap dalam contoh sepele Anda yang tidak melakukan pekerjaan nyata.
dobey
Poin yang valid, tapi saya tidak berpikir contoh sepele pantas mengirim pemberitahuan melalui DBus (yang saya pikir harus dilakukan aplikasi non-sepele)
RobotHumans
Hm, menjalankan async_calldalam contoh ini bekerja untuk saya, tetapi membawa kekacauan ketika saya porting ke aplikasi saya dan saya menambahkan slow_loadfungsi nyata yang saya punya.
David Planella