วิธีการทำงานแบบอะซิงโครนัสในแอพ 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มาก่อนในเรื่องแบบนี้ แต่ฉันกังวลว่ารหัสของมันไม่ได้ถูกแตะต้องใน 3 ปีเลยก็ถูกส่งไปยัง GObject Introspection เพียงลำพัง สิ่งสำคัญที่สุดคือไม่มีใน Ubuntu 12.04 อีกต่อไป ดังนั้นฉันกำลังมองหาวิธีที่ง่ายในการทำงานแบบอะซิงโครนัสไม่ว่าจะเป็นแบบ Python มาตรฐานหรือในแบบมาตรฐาน GObject / GTK +

แก้ไข: นี่คือรหัสบางส่วนพร้อมตัวอย่างของสิ่งที่ฉันพยายามทำ ฉันได้ลองpython-deferตามที่แนะนำในความคิดเห็น แต่ฉันไม่สามารถจัดการกับภารกิจที่มีความยาวแบบอะซิงโครนัสและปล่อยให้โหลด UI โดยไม่ต้องรอให้เสร็จ เรียกดูรหัสการทดสอบ

มีวิธีที่ง่ายและใช้กันอย่างแพร่หลายในการทำงานแบบอะซิงโครนัสและรับการแจ้งเตือนเมื่องานเสร็จหรือไม่


มันไม่ได้เป็นตัวอย่างที่ดี แต่ฉันค่อนข้างแน่ใจว่านี่คือสิ่งที่คุณกำลังมองหา: raw.github.com/gist/1132418/…
RobotHumans

เยี่ยมฉันคิดว่าasync_callฟังก์ชั่นของคุณอาจเป็นสิ่งที่ฉันต้องการ คุณจะช่วยขยายความคิดออกไปเล็กน้อยและเพิ่มคำตอบเพื่อที่ฉันจะได้ยอมรับและให้เครดิตคุณหลังจากที่ฉันทดสอบ ขอบคุณ!
David Planella

1
เป็นคำถามที่ดีมีประโยชน์มาก! ;-)
RafałCieślak

คำตอบ:


15

ปัญหาของคุณเป็นปัญหาที่พบบ่อยมากดังนั้นจึงมีวิธีแก้ปัญหามากมาย (เพิงคิวกับมัลติโพรเซสซิงหรือเธรดพูลงาน ... )

เนื่องจากเป็นเรื่องธรรมดาดังนั้นจึงมีโซลูชันการสร้างแบบหลาม (ใน 3.2 แต่กลับไปที่นี่: http://pypi.python.org/pypi/futures ) ที่เรียกว่า concurrent.futures 'อนาคต' มีให้บริการในหลายภาษาดังนั้นงูหลามจึงเรียกมันว่าเดียวกัน นี่คือการโทรทั่วไป (และนี่คือตัวอย่างแบบเต็มของคุณอย่างไรก็ตามส่วน db จะถูกแทนที่ด้วยโหมดสลีปดูสาเหตุด้านล่าง)

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_loadจากฐานข้อมูลนั้นไม่สามารถเลือกได้ซึ่งหมายความว่าวัตถุนั้นไม่สามารถผ่านระหว่างกระบวนการได้อย่างง่ายดาย ดังนั้น: ไม่มีการประมวลผลหลายตัวกับผลลัพธ์ของซอฟต์แวร์ศูนย์กลาง!
  3. ไลบรารี่ที่คุณเรียกใช้ (softwarecenter.db) ไม่ใช่ threadsafe (ดูเหมือนจะรวม gtk หรือคล้ายกัน) ดังนั้นการเรียกเมธอดเหล่านี้ในเธรดจะส่งผลให้เกิดพฤติกรรมแปลก ๆ (ในการทดสอบของฉันทุกอย่างจาก เลิกโดยไม่มีผลลัพธ์) ดังนั้น: ไม่มีเธรดที่มีซอฟต์แวร์ศูนย์
  4. การโทรกลับแบบอะซิงโครนัสทุกครั้งใน gtk ไม่ควรทำอะไรนอกจากการโทรกลับซึ่งจะถูกเรียกใน glib mainloop ดังนั้น: ไม่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 ของคุณได้รับการว่าจ้างคุณสามารถขอให้ผู้พัฒนาซอฟต์แวร์ศูนย์โดยตรงเพิ่มเอกสารไปยังซอฟต์แวร์ของพวกเขา (เช่นระบุว่ามันไม่ปลอดภัยเธรด) และดียิ่งขึ้นทำให้ softwarecenter threadsafe

ขณะที่ทราบ: การแก้ปัญหาที่ได้รับจากผู้อื่น ( Gio.io_scheduler_push_job, async_call) ทำงานด้วยแต่ไม่ได้มีtime.sleep softwarecenter.dbนี้เป็นเพราะมันเดือดลงไปทุกหัวข้อหรือกระบวนการและหัวข้อที่จะได้ทำงานกับ GTK softwarecenterและ


ขอบคุณ! ฉันจะยอมรับคำตอบของคุณเพราะจะให้รายละเอียดกับฉันมากว่าทำไมมันถึงใช้ไม่ได้ แต่น่าเสียดายที่ฉันไม่สามารถใช้ซอฟต์แวร์ที่ไม่ได้บรรจุสำหรับ Ubuntu 12.04 ใน app ของฉัน (ก็คือสำหรับ Quantal แม้ว่าlaunchpad.net/ubuntu/+source/python-concurrent.futures ) ดังนั้นฉันคิดว่าฉันติดอยู่กับที่ไม่สามารถ เพื่อเรียกใช้งานของฉันแบบอะซิงโครนัส เกี่ยวกับบันทึกย่อที่จะพูดคุยกับนักพัฒนาซอฟต์แวร์ศูนย์ฉันอยู่ในตำแหน่งเดียวกับอาสาสมัครที่มีส่วนร่วมในการเปลี่ยนแปลงรหัสและเอกสารหรือพูดคุยกับพวกเขา :-)
David Planella

GIL ได้รับการปล่อยตัวในช่วง IO ดังนั้นจึงเป็นเรื่องดีที่จะใช้เธรด แม้ว่าจะไม่จำเป็นถ้าใช้ async IO
jfs

10

นี่คือตัวเลือกอื่นโดยใช้ I / O Scheduler ของ GIO (ฉันไม่เคยใช้มาก่อนจาก 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()

ดูเพิ่มเติมที่ GIO.io_scheduler_job_send_to_mainloop () หากคุณต้องการเรียกใช้บางอย่างในเธรดหลักเมื่อ slow_stuff เสร็จสิ้น
Siegfried Gevatter

ขอบคุณ Sigfried สำหรับคำตอบและตัวอย่าง น่าเสียดายที่ดูเหมือนกับงานปัจจุบันของฉันฉันไม่มีโอกาสใช้ Gio API เพื่อให้ทำงานแบบอะซิงโครนัส
David Planella

สิ่งนี้มีประโยชน์จริง ๆ แต่เท่าที่ฉันสามารถบอก Gio.io_scheduler_job_send_to_mainloop ไม่มีอยู่ใน Python :(
sil

2

คุณยังสามารถใช้ GLib.idle_add (การโทรกลับ) เพื่อเรียกใช้งานที่ใช้เวลานานเมื่อ GLib Mainloop เสร็จสิ้นเหตุการณ์ทั้งหมดที่มีลำดับความสำคัญสูงกว่า (ซึ่งฉันเชื่อว่ารวมถึงการสร้าง UI)


ขอบคุณไมค์ ใช่แล้วนั่นจะช่วยในการเริ่มต้นภารกิจเมื่อ UI พร้อมใช้งาน แต่ในทางกลับกันฉันเข้าใจว่าเมื่อcallbackถูกเรียกมันจะทำแบบซิงโครนัสดังนั้นจึงปิดกั้น UI ใช่ไหม
David Planella

idle_add ไม่ทำงานเช่นนั้น การบล็อกการโทรใน idle_add ยังคงเป็นสิ่งที่ไม่ดีและจะป้องกันไม่ให้มีการอัพเดต UI และแม้กระทั่ง API แบบอะซิงโครนัสก็ยังสามารถปิดกั้นได้ซึ่งวิธีเดียวที่จะหลีกเลี่ยงการบล็อก UI และงานอื่น ๆ คือทำในเธรดพื้นหลัง
dobey

เป็นการดีที่คุณจะแบ่งงานช้าของคุณออกเป็นชิ้น ๆ เพื่อให้คุณสามารถเรียกใช้บิตในการโทรกลับไม่ได้ใช้งานกลับมา บน.
Siegfried Gevatter

gotcha ด้วยidle_addคือค่าตอบแทนของการติดต่อกลับมีความสำคัญ ถ้ามันเป็นจริงมันจะถูกเรียกอีกครั้ง
Flimm

2

ใช้ introspected GioAPI เพื่ออ่านไฟล์ด้วยวิธีการไม่ตรงกันของตนและเมื่อมีการโทรเริ่มต้นทำมันให้หมดเวลากับGLib.timeout_add_seconds(3, call_the_gio_stuff)ที่เป็นฟังก์ชั่นซึ่งผลตอบแทนcall_the_gio_stuffFalse

จำเป็นต้องเพิ่มการหมดเวลาที่นี่ (อาจต้องใช้จำนวนวินาทีที่แตกต่างกัน) เนื่องจากในขณะที่การเรียก Gio async เป็นแบบอะซิงโครนัสพวกเขาจะไม่ปิดกั้นซึ่งหมายความว่ากิจกรรมบนฮาร์ดดิสก์ของการอ่านไฟล์ขนาดใหญ่หรือขนาดใหญ่ จำนวนไฟล์สามารถทำให้ UI ที่ถูกบล็อคได้เนื่องจาก UI และ I / O ยังคงอยู่ในเธรด (หลัก) เดียวกัน

หากคุณต้องการเขียนฟังก์ชั่นของคุณเองให้เป็นแบบซิงค์และรวมเข้ากับลูปหลักโดยใช้ไฟล์ I / O API ของ Python คุณจะต้องเขียนโค้ดเป็น GObject หรือส่งผ่านการโทรกลับหรือใช้python-deferเพื่อช่วยคุณ ทำมัน. แต่เป็นการดีที่สุดที่จะใช้ Gio ที่นี่เนื่องจากสามารถนำคุณลักษณะที่ดีมากมายมาให้คุณโดยเฉพาะอย่างยิ่งหากคุณกำลังเปิดไฟล์ / บันทึกข้อมูลใน UX


ขอบคุณ @dobey ฉันไม่ได้อ่านไฟล์จากดิสก์โดยตรงจริง ๆ ฉันน่าจะทำให้เห็นชัดขึ้นในโพสต์ดั้งเดิม งานที่ต้องทำงานเป็นเวลานานคือการอ่านฐานข้อมูล Software Center ตามคำตอบของaskubuntu.com/questions/139032/ …ดังนั้นฉันไม่แน่ใจว่าฉันจะสามารถใช้GioAPI ได้ สิ่งที่ฉันสงสัยคือว่ามีวิธีในการรันงานที่ใช้งานทั่วไปแบบอะซิงโครนัสในแบบเดียวกับที่ GTask เคยทำหรือไม่
David Planella

ฉันไม่ทราบว่า GTask คืออะไร แต่ถ้าคุณหมายถึงgtask.sourceforge.netฉันไม่คิดว่าคุณควรจะใช้มัน หากเป็นอย่างอื่นฉันก็ไม่รู้ว่ามันคืออะไร แต่ดูเหมือนว่าคุณจะต้องใช้เส้นทางที่สองที่ฉันพูดถึงและใช้ API แบบอะซิงโครนัสเพื่อห่อโค้ดนั้นหรือทำทุกอย่างในเธรด
dobey

มีลิงก์ไปยังคำถาม GTask คือ (เคย): chergert.github.com/gtask
David Planella

1
อานั่นดูคล้ายกับ API ที่จัดทำโดย python-defer (และ API ที่เลื่อนออกไปของ twisted) บางทีคุณควรดูโดยใช้ python-defer?
dobey

1
คุณยังคงต้องหน่วงเวลาการเรียกใช้จนกว่าจะเกิดเหตุการณ์ลำดับความสำคัญหลักโดยใช้ GLib.idle_add () ตัวอย่างเช่น เช่นนี้: pastebin.ubuntu.com/1011660
dobey

1

ฉันคิดว่ามันสังเกตได้ว่านี่เป็นวิธีที่ซับซ้อนในการทำตามที่ @ hall แนะนำ

เป็นหลักคุณได้รับการเรียกใช้แล้วเรียกใช้ฟังก์ชันของ 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()

หมายเหตุเพิ่มเติมคุณต้องปล่อยให้เธรดอื่น ๆ เสร็จสิ้นก่อนที่มันจะจบลงอย่างถูกต้องหรือตรวจสอบไฟล์ล็อคในเธรดลูก

แก้ไขเพื่อแสดงความคิดเห็นที่อยู่: ตอนแรกผมลืม
GObject.threads_init()เห็นได้ชัดเมื่อปุ่มยิงมันเริ่มต้นเกลียวสำหรับฉัน นี่มันหลอกลวงความผิดของฉัน

โดยทั่วไปโฟลว์จะสร้างหน้าต่างในหน่วยความจำเปิดใช้เธรดอื่นทันทีเมื่อเธรดอัพเดตปุ่มเสร็จสมบูรณ์ ฉันได้เพิ่มโหมดสลีปเพิ่มเติมก่อนที่ฉันจะเรียกว่า Gtk.main เพื่อตรวจสอบว่าการอัปเดตเต็มอาจเรียกใช้ก่อนที่จะดึงหน้าต่างออกมา ฉันยังแสดงความคิดเห็นเพื่อตรวจสอบว่าการเปิดตัวเธรดไม่ขัดขวางการวาดหน้าต่างเลย


1
ขอบคุณ ฉันไม่แน่ใจว่าฉันสามารถติดตามได้ สำหรับหนึ่งฉันคาดว่าslow_loadจะมีการดำเนินการในไม่ช้าหลังจากที่ UI เริ่มทำงาน แต่ดูเหมือนว่าจะไม่ถูกเรียกเว้นแต่จะคลิกปุ่มซึ่งทำให้ฉันสับสนเล็กน้อยเนื่องจากฉันคิดว่าจุดประสงค์ของปุ่มนั้นเป็นเพียงการแสดงภาพ ของสถานะของงาน
David Planella

ขออภัยฉันพลาดหนึ่งบรรทัด นั่นมัน ฉันลืมบอก GObject ให้พร้อมสำหรับเธรด
RobotHumans

แต่คุณกำลังโทรเข้าสู่ลูปหลักจากเธรดซึ่งอาจทำให้เกิดปัญหาแม้ว่าพวกเขาอาจไม่ได้รับการเปิดเผยในตัวอย่างเล็กน้อยของคุณซึ่งไม่ได้ทำงานจริงใด ๆ
dobey

จุดที่ถูกต้อง แต่ฉันไม่คิดว่าเป็นตัวอย่างเล็ก ๆ น้อย ๆ ที่สมควรได้รับการส่งการแจ้งเตือนผ่านทาง DBus (ซึ่งฉันคิดว่าควรจะมีแอพที่ไม่ใช่เรื่องเล็ก ๆ น้อย ๆ )
RobotHumans

อืมการทำงานasync_callในตัวอย่างนี้ใช้ได้สำหรับฉัน แต่มันจะทำให้เกิดการทำร้ายร่างกายเมื่อฉันย้ายมันไปยังแอปของฉันและฉันเพิ่มslow_loadฟังก์ชั่นจริงที่ฉันมี
David Planella
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.