Python GObject Introspection uygulamalarında eşzamansız görevler nasıl çalıştırılır


16

Başlangıçta diskten önemsiz miktarda veri okuması gereken bir Python + GObject uygulaması yazıyorum. Veriler senkronize olarak okunur ve okuma işlemini bitirmek yaklaşık 10 saniye sürer, bu süre zarfında kullanıcı arayüzünün yüklenmesi gecikir.

Görevi eşzamansız olarak çalıştırmak ve hazır olduğunda UI'yi engellemeden bir bildirim almak istiyorum:

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

Geçmişte bu tür bir şey için GTask kullandım , ancak kodunun 3 yıl içinde GObject Introspection'a taşınmasına izin verilmediğinden endişeleniyorum. En önemlisi, artık Ubuntu 12.04'te mevcut değil. Bu yüzden, görevleri standart bir Python veya GObject / GTK + standart şekilde eşzamansız olarak çalıştırmanın kolay bir yolunu arıyorum.

Düzenleme: İşte yapmaya çalıştığım bir örnek bazı kod. python-deferYorumlarda önerildiği gibi denedim , ancak uzun görevi asenkron olarak çalıştırmayı başaramadım ve bitmesini beklemek zorunda kalmadan UI yüklemesine izin veremedim. Test koduna göz atın .

Zaman uyumsuz görevleri yürütmenin ve tamamlandığında bildirim almanın kolay ve yaygın olarak kullanılan bir yolu var mı?


Güzel bir örnek değil, ama aradığınızın bu olduğundan eminim: raw.github.com/gist/1132418/…
RobotHumans

Harika, async_callfonksiyonunun ihtiyacım olan şey olabileceğini düşünüyorum . Biraz genişletip bir cevap ekleyebilir misiniz, böylece test ettikten sonra kabul edebilir ve size kredi verebilir miyim? Teşekkürler!
David Planella

1
Harika soru, çok kullanışlı! ;-)
Rafał Cieślak

Yanıtlar:


15

Sorununuz çok yaygındır, bu nedenle tonlarca çözüm vardır (barakalar, çok işlemli veya diş açılmış kuyruklar, işçi havuzları, ...)

Çok yaygın olduğu için, concurrent.futures adı verilen bir python yerleşik çözümü de (3.2'de ama burada desteklenmektedir: http://pypi.python.org/pypi/futures ). 'Futures' birçok dilde mevcuttur, bu nedenle python bunları aynı şekilde adlandırır. İşte tipik çağrılar (ve tam örneğiniz burada , ancak db kısmı uyku ile değiştirildi, nedenine bakın).

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)

Şimdi probleminize, basit örneğinizin önerdiğinden çok daha karmaşık. Genel olarak, bunu çözmek için iş parçacıklarınız veya işlemleriniz vardır, ancak işte örneğinizin neden bu kadar karmaşık olduğu:

  1. En Python uygulamaları ipler yapan bir GIL sahip değil tamamen multicores kullanmaktadır. Yani: python ile konuları kullanmayın!
  2. slow_loadDB'den dönmek istediğiniz nesneler alınamaz, yani işlemler arasında kolayca geçilemez. Yani: yazılım merkezi sonuçları ile çoklu işlem yok!
  3. Aradığınız program (softwarecenter.db) threadsafe değildir (gtk veya benzeri gibi görünüyor), bu nedenle bu yöntemleri bir iş parçacığında çağırmak garip davranışlarla sonuçlanır (testimde, 'çekirdek dökümünden' işe yarar ' sonuçsuz bırakma). Yani: yazılım merkezi ile iplik yok.
  4. Gtk içindeki her eşzamansız geri arama , glib ana döngüsünde çağrılacak bir geri çağrıyı kaydetmek dışında hiçbir şey yapmamalıdır. Yani: hayır print, geri arama eklemek dışında gtk durumu değişmiyor!
  5. Gtk ve benzerleri kutudan çıkan dişlerle çalışmaz. Sen yapmak gerekir threads_initve bir gtk veya benzer bir yöntemi çağırmak varsa, bu oldu önceki sürümlerinde bu yöntemi (korumak zorunda gtk.gdk.threads_enter(), gtk.gdk.threads_leave()örnek gstreamer için bkz. Http://pygstdocs.berlios.de/pygst-tutorial/playbin. html ).

Size şu öneriyi verebilirim:

  1. slow_loadSeçilebilir sonuçları döndürmek ve vadeli işlemleri süreçlerle kullanmak için yeniden yazın .
  2. Softwarecenter'dan python-apt veya benzerine geçin (muhtemelen bundan hoşlanmıyorsunuzdur). Ancak Canonical tarafından çalıştırıldığınızdan beri, softwarecenter geliştiricilerinden yazılımlarına doğrudan belge eklemelerini isteyebilirsiniz (örneğin, iş parçacığının güvenli olmadığını belirterek) ve daha da iyisi, softwarecenter iş parçacığı güvenliği sağlar.

Bir not olarak: Başkaları tarafından verilen çözümlerin ( Gio.io_scheduler_push_job, async_call) yapmak ile çalışmayıtime.sleep ancak birlikte softwarecenter.db. Bunun nedeni, gtk ve ile çalışmamak için tüm iş parçacıklarına veya işlemlere ve iş parçacıklarına kadar kaynar olmasıdır softwarecenter.


Teşekkürler! Cevabınızı kabul edilemediğine dair beni çok ayrıntılı olarak gösterdiğinden cevabınızı kabul edeceğim. Ne yazık ki, uygulamamda Ubuntu 12.04 için paketlenmemiş yazılımı kullanamıyorum (Qpadtal için, launchpad.net/ubuntu/+source/python-concurrent.futures olsa da ), bu yüzden mümkün olamadım görevimi eşzamansız olarak çalıştırmak için. Yazılım Merkezi geliştiricileriyle konuşma notu ile ilgili olarak, kod ve belgelerdeki değişikliklere katkıda bulunmak veya onlarla konuşmak için herhangi bir gönüllüle aynı pozisyondayım :-)
David Planella

GIL G / Ç sırasında serbest bırakılır, bu nedenle dişleri kullanmak mükemmeldir. Yine de async IO kullanılması gerekli değildir.
jfs

10

GIO'nun G / Ç Zamanlayıcısını kullanan başka bir seçenek daha (Python'dan daha önce hiç kullanmadım, ancak aşağıdaki örnek iyi çalışıyor gibi görünüyor).

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 bittikten sonra ana iş parçacığında bir şey çalıştırmak istiyorsanız, bkz. GIO.io_scheduler_job_send_to_mainloop ().
Siegfried Gevatter

Yanıt ve örnek için teşekkürler Sigfried. Ne yazık ki, şu anki görevim ile Gio API'sını senkronize olmayan bir şekilde çalıştırmak için kullanma şansım yok gibi görünüyor.
David Planella

Bu gerçekten yararlıydı, ancak Gio.io_scheduler_job_send_to_mainloop'un Python'da mevcut olmadığını söyleyebildiğim kadar :(
sil

2

GLib Mainloop tüm yüksek öncelikli olaylarını (kullanıcı arayüzünü oluşturmayı da içerir) bitirdiğinde uzun süren görevi çağırmak için GLib.idle_add (geri çağrı) da kullanabilirsiniz.


Teşekkürler Mike. Evet, kullanıcı arayüzü hazır olduğunda görevin başlatılmasında kesinlikle yardımcı olacaktır. Ancak diğer yandan, callbackçağrıldığında bunun eşzamanlı olarak yapılacağını, dolayısıyla kullanıcı arayüzünü engelleyeceğini anlıyorum , değil mi?
David Planella

İdle_add bu şekilde çalışmaz. Bir idle_add içinde engelleme çağrıları yapmak hala kötü bir şeydir ve kullanıcı arayüzünde güncelleme yapılmasını önleyecektir. Ayrıca, kullanıcı arayüzünü ve diğer görevleri engellemekten kaçınmanın tek yolunun bir arka plan iş parçacığında yapmak olduğu zaman, eşzamansız API bile engelleme olabilir.
dobey

İdeal olarak, yavaş görevinizi parçalara böldünüz, böylece boş bir geri aramada biraz çalıştırabilir, geri dönebilir (ve UI geri çağrıları gibi diğer şeylerin çalışmasına izin verebilirsiniz), geri arama tekrar çağrıldıktan sonra biraz daha çalışma yapmaya devam edebilirsiniz. üzerinde.
Siegfried Gevatter

Bununla ilgili bir idle_addsorun, geri aramanın dönüş değerinin önemli olmasıdır. Doğruysa, tekrar çağrılacaktır.
Flimm

2

İçebakış kullanın Gioonun asenkron yöntemlerle, bir dosyayı okumak için API ve ilk arama yaparken, bir zaman aşımı süresi olarak bunuGLib.timeout_add_seconds(3, call_the_gio_stuff)call_the_gio_stuff geri dönen bir işlevin olduğu birFalse .

Buradaki zaman aşımı eklemek gerekir (yine de farklı sayıda saniye gerekebilir), çünkü Gio zaman uyumsuz çağrıları eşzamansız olsa da, engellemezler, yani büyük bir dosyayı okuma veya UI ve G / Ç hala aynı (ana) iş parçacığında olduğundan, dosya sayısı engellenen kullanıcı arayüzüne neden olabilir.

Python'un dosya G / Ç API'lerini kullanarak kendi işlevlerinizi zaman uyumsuz olarak yazmak ve ana döngü ile entegre etmek istiyorsanız, kodu GObject olarak yazmanız veya geri aramaları iletmeniz veya python-defersize yardımcı olmak için kullanmanız gerekir. yap. Ancak Gio'yu burada kullanmak en iyisidir, çünkü özellikle UX'de dosya açma / kaydetme yapıyorsanız size birçok güzel özellik getirebilir.


Teşekkürler @dobey. Aslında doğrudan diskten bir dosya okumuyorum, muhtemelen orijinal yazı daha net yapmalıydım. Çalıştığım uzun süredir devam eden görev, askubuntu.com/questions/139032/… adresine verilen cevaba göre Yazılım Merkezi veritabanını okuyor , bu yüzden GioAPI'yi kullanabileceğimden emin değilim . Merak ettiğim, GTask'ın yaptığı gibi eşzamanlı olarak herhangi bir genel uzun süren görevi çalıştırmanın bir yolu olup olmadığıdır.
David Planella

GTask'ın tam olarak ne olduğunu bilmiyorum, ama gtask.sourceforge.net demek istiyorsanız bunu kullanmanız gerektiğini düşünmüyorum. Başka bir şeyse, ne olduğunu bilmiyorum. Ancak, bahsettiğim ikinci rotayı izlemeniz ve bu kodu sarmak veya sadece bir iş parçacığında yapmak için bazı eşzamansız API uygulamak zorunda kalacaksınız.
dobey

Soruda bir bağlantı var. GTask (oldu): chergert.github.com/gtask
David Planella

1
Ah, bu python-defer (ve twisted'ın ertelenmiş API'sı) tarafından sağlanan API'ye çok benziyor. Belki de python-defer kullanmaya bakmalısın?
dobey

1
Örneğin, GLib.idle_add () öğesini kullanarak ana öncelikli olaylar gerçekleşene kadar çağrıyı ertelemeniz gerekir. Bunun gibi: pastebin.ubuntu.com/1011660
dobey 28:12

1

Bunun, mhall'in önerilerini yapmak için kıvrımlı bir yol olduğunu belirtmek gerektiğini düşünüyorum.

Aslında, bu bir çalışma var sonra async_call işlevini çalıştırın.

Nasıl çalıştığını görmek istiyorsanız, uyku zamanlayıcısı ile oynayabilir ve düğmeyi tıklamaya devam edebilirsiniz. Örnek kod olması dışında @ mhall'ın cevabı ile aynıdır.

Buna dayanarak benim işim değil.

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

Ek not, diğer iş parçacığının düzgün bir şekilde sonlanmadan önce bitmesine izin vermeniz veya alt dizinizde bir file.lock olup olmadığını denetlemeniz gerekir.

Yorum için adresi düzenle:
Başlangıçta unuttumGObject.threads_init() . Açıkçası düğme ateş ettiğinde, benim için ipliği başlattı. Bu benim için hatayı gizledi.

Genellikle akış, bellekteki pencereyi oluşturmaktır, iplik tamamlandığında düğmeyi hemen güncelleyin, diğer iş parçacığını başlatın. Tam güncelleme, pencere çizilmeden önce tam güncellemenin çalıştığını doğrulamak için Gtk.main'i bile çağırmadan önce ek bir uyku ekledim. Ben de iplik fırlatma pencere çizim hiç engellemediğini doğrulamak için yorumladı.


1
Teşekkürler. Takip edebileceğimden emin değilim. Birincisi, slow_loadUI başladıktan kısa bir süre sonra yürütülmeyi beklerdim, ancak düğmenin amacı sadece görsel gösterge sağlamak olduğunu düşündüğüm için, düğmeyi tıklatmadıkça beni biraz şaşırtan sürece asla çağrılmaz gibi görünüyor görevin durumu.
David Planella

Üzgünüm, bir hattı kaçırdım. Bunu yaptı. GObject'e konulara hazırlanmasını söylemeyi unuttum.
RobotHumans

Ancak, bir iş parçacığından ana döngüye giriyorsunuz, bu da sorunlara neden olabilir, ancak gerçek bir iş yapmayan önemsiz örneğinizde kolayca ortaya çıkmayabilirler.
dobey

Geçerli bir nokta, ancak önemsiz bir örnek DBus üzerinden bildirim göndermeyi hak etmediğini düşünmüyordum (önemsiz bir uygulamanın yapması gerektiğini düşünüyorum)
RobotHumans

Hm, async_callbu örnekte çalışmak benim için işe yarıyor, ancak uygulamama taşıdığımda kargaşa getiriyor ve sahip olduğum gerçek slow_loadişlevi ekliyorum .
David Planella
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.