Cách chạy các tác vụ không đồng bộ trong các ứng dụng Introspection của Python GObject


16

Tôi đang viết một ứng dụng Python + GObject cần đọc một lượng dữ liệu không hề nhỏ từ đĩa khi bắt đầu. Dữ liệu được đọc đồng bộ và mất khoảng 10 giây để hoàn thành thao tác đọc, trong thời gian đó, quá trình tải UI bị trì hoãn.

Tôi muốn chạy tác vụ không đồng bộ và nhận thông báo khi nó sẵn sàng, mà không chặn UI, ít nhiều như:

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

Tôi đã sử dụng GTask trong quá khứ cho loại điều này, nhưng tôi lo ngại rằng mã của nó đã bị chạm trong 3 năm, chứ chưa nói đến việc chuyển sang GObject Introspection. Quan trọng nhất, nó không còn có sẵn trong Ubuntu 12.04. Vì vậy, tôi đang tìm kiếm một cách dễ dàng để chạy các tác vụ không đồng bộ, theo cách tiêu chuẩn của Python hoặc theo cách tiêu chuẩn GObject / GTK +.

Chỉnh sửa: đây là một số mã với một ví dụ về những gì tôi đang cố gắng làm. Tôi đã cố gắng python-defernhư được đề xuất trong các nhận xét, nhưng tôi không thể quản lý để chạy tác vụ dài một cách không đồng bộ và để giao diện người dùng tải mà không phải đợi nó hoàn thành. Duyệt mã kiểm tra .

Có cách nào dễ dàng và được sử dụng rộng rãi để chạy các tác vụ không đồng bộ và được thông báo khi chúng kết thúc không?


Đây không phải là một ví dụ hay, nhưng tôi khá chắc chắn đây là thứ bạn đang tìm kiếm: raw.github.com/gist/1132418/ Kẻ
RobotHumans

Thật tuyệt, tôi nghĩ async_callchức năng của bạn có thể là những gì tôi cần. Bạn có muốn mở rộng về nó một chút và thêm một câu trả lời, để tôi có thể chấp nhận nó và ghi có cho bạn sau khi tôi kiểm tra nó không? Cảm ơn!
David Planella

1
Câu hỏi tuyệt vời, rất hữu ích! ;-)
Rafał Cieślak

Câu trả lời:


15

Vấn đề của bạn là một vấn đề rất phổ biến, do đó, có rất nhiều giải pháp (nhà kho, hàng đợi với đa xử lý hoặc phân luồng, nhóm công nhân, ...)

Vì nó rất phổ biến, nên cũng có một giải pháp tích hợp python (trong 3.2, nhưng được nhập vào đây: http://pypi.python.org/pypi/futures ) được gọi là concản.futures. "Tương lai" có sẵn trong nhiều ngôn ngữ, do đó python gọi chúng là giống nhau. Dưới đây là các cuộc gọi tiêu biểu (và đây là ví dụ đầy đủ của bạn , tuy nhiên, phần db được thay thế bằng chế độ ngủ, xem bên dưới tại sao).

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)

Bây giờ với vấn đề của bạn, nó phức tạp hơn nhiều so với ví dụ đơn giản của bạn cho thấy. Nói chung, bạn có các luồng hoặc các quy trình để giải quyết vấn đề này, nhưng đây là lý do tại sao ví dụ của bạn rất phức tạp:

  1. Hầu hết các triển khai Python đều có GIL, điều này làm cho các luồng không sử dụng hoàn toàn đa lõi. Vì vậy: không sử dụng chủ đề với python!
  2. Các đối tượng bạn muốn quay trở lại slow_loadtừ DB không thể chọn được, điều đó có nghĩa là chúng không thể được chuyển qua lại giữa các tiến trình. Vì vậy: không có đa xử lý với kết quả của softwarecenter!
  3. Thư viện mà bạn gọi (softwarecenter.db) không phải là chủ đề an toàn (dường như bao gồm gtk hoặc tương tự), do đó, việc gọi các phương thức này trong một luồng dẫn đến hành vi lạ (trong thử nghiệm của tôi, mọi thứ từ 'nó hoạt động' trên 'kết xuất lõi' đến đơn giản bỏ cuộc mà không có kết quả). Vì vậy: không có chủ đề với softwarecenter.
  4. Mỗi cuộc gọi lại không đồng bộ trong gtk không nên làm bất cứ điều gì ngoại trừ loại bỏ một cuộc gọi lại sẽ được gọi trong mainloop glib. Vì vậy: không print, không có trạng thái gtk thay đổi, ngoại trừ thêm một cuộc gọi lại!
  5. Gtk và như nhau không hoạt động với các chủ đề ra khỏi hộp. Bạn cần phải làm threads_initvà nếu bạn gọi một phương thức gtk hoặc tương tự, bạn phải bảo vệ phương thức đó (trong các phiên bản trước đó gtk.gdk.threads_enter(), gtk.gdk.threads_leave()xem ví dụ: gpyer: http://pygstdocs.berlios.de/pygst-tutorial/playbin. html ).

Tôi có thể cho bạn gợi ý sau:

  1. Viết lại của bạn slow_loadđể trả về kết quả có thể chọn và sử dụng tương lai với các quy trình.
  2. Chuyển từ softwarecenter sang python-apt hoặc tương tự (bạn có thể không thích điều đó). Nhưng do Canonical sử dụng, bạn có thể yêu cầu các nhà phát triển phần mềm trực tiếp thêm tài liệu vào phần mềm của họ (ví dụ: nói rằng nó không an toàn cho luồng) và thậm chí tốt hơn, làm cho các chủ đề của phần mềm bảo mật.

Như một lưu ý: các giải pháp được đưa ra bởi những người khác ( Gio.io_scheduler_push_job, async_call) thực hiện với time.sleepnhưng không phải với softwarecenter.db. Điều này là bởi vì tất cả tập trung vào các luồng hoặc quy trình và luồng để không hoạt động với gtk và softwarecenter.


Cảm ơn! Tôi sẽ chấp nhận câu trả lời của bạn vì nó chỉ cho tôi rất nhiều chi tiết về lý do tại sao nó không thể thực hiện được. Thật không may, tôi không thể sử dụng phần mềm không được đóng gói cho Ubuntu 12.04 trong ứng dụng của mình (nó dành cho Quantal, mặc dù launchpad.net/ubfox/+source/python-concản.futures ), vì vậy tôi đoán rằng tôi bị mắc kẹt vì không thể để chạy nhiệm vụ của tôi không đồng bộ. Về lưu ý khi nói chuyện với các nhà phát triển Trung tâm phần mềm, tôi ở vị trí tương tự như bất kỳ tình nguyện viên nào đóng góp thay đổi mã và tài liệu hoặc nói chuyện với họ :-)
David Planella

GIL được phát hành trong IO nên việc sử dụng các luồng là hoàn toàn tốt. Mặc dù không cần thiết nếu IO async được sử dụng.
jfs

10

Đây là một tùy chọn khác sử dụng Bộ lập lịch I / O của GIO (Tôi chưa bao giờ sử dụng nó trước đây từ Python, nhưng ví dụ dưới đây dường như chạy tốt).

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

Xem thêm GIO.io_scheduler_job_send_to_mainloop (), nếu bạn muốn chạy một cái gì đó trong luồng chính sau khi Slow_ ware kết thúc.
Siegfried Gevatter

Cảm ơn Sigfried cho câu trả lời và ví dụ. Thật không may, dường như với nhiệm vụ hiện tại của tôi, tôi không có cơ hội sử dụng API Gio để làm cho nó chạy không đồng bộ.
David Planella

Điều này thực sự hữu ích, nhưng theo như tôi có thể nói với Gio.io_scheduler_job_send_to_mainloop không tồn tại trong Python :(
sil

2

Bạn cũng có thể sử dụng GLib.idle_add (gọi lại) để gọi tác vụ dài hạn một khi GLib Mainloop hoàn thành tất cả các sự kiện ưu tiên cao hơn (mà tôi tin bao gồm xây dựng giao diện người dùng).


Cảm ơn Mike. Vâng, điều đó chắc chắn sẽ giúp bắt đầu nhiệm vụ khi UI đã sẵn sàng. Nhưng mặt khác, tôi hiểu rằng khi callbackđược gọi, điều đó sẽ được thực hiện đồng bộ, do đó chặn UI, phải không?
David Planella

Idle_add không hoạt động như vậy. Thực hiện chặn các cuộc gọi trong idle_add vẫn là một việc không hay và nó sẽ ngăn các cập nhật cho UI xảy ra. Và ngay cả API không đồng bộ vẫn có thể bị chặn, trong đó cách duy nhất để tránh chặn UI và các tác vụ khác, là thực hiện nó trong một luồng nền.
dobey

Lý tưởng nhất là bạn chia nhiệm vụ chậm của mình thành nhiều phần, vì vậy bạn có thể chạy một chút của nó trong một cuộc gọi lại nhàn rỗi, trả lại (và để những thứ khác như cuộc gọi lại UI chạy), tiếp tục thực hiện thêm một số công việc sau khi cuộc gọi lại của bạn được gọi lại, và vì vậy trên.
Siegfried Gevatter

Một gotcha với idle_addlà giá trị trả về của các cuộc gọi lại có vấn đề. Nếu nó đúng, nó sẽ được gọi lại.
Flimm

2

Sử dụng các introspected GioAPI để đọc một tập tin, với các phương pháp không đồng bộ của nó, và khi thực hiện cuộc gọi đầu tiên, làm nó như là một thời gian chờ với GLib.timeout_add_seconds(3, call_the_gio_stuff)nơi call_the_gio_stufflà một chức năng mà trở về False.

Tuy nhiên, thời gian chờ ở đây là cần thiết để thêm (một số giây khác nhau có thể được yêu cầu), bởi vì trong khi các cuộc gọi không đồng bộ Gio không đồng bộ, chúng không chặn, có nghĩa là hoạt động đĩa nặng của việc đọc một tệp lớn hoặc lớn số lượng tệp, có thể dẫn đến UI bị chặn, vì UI và I / O vẫn nằm trong cùng một luồng (chính).

Nếu bạn muốn viết các hàm của riêng mình thành không đồng bộ và tích hợp với vòng lặp chính, sử dụng API I / O của tệp Python, bạn sẽ phải viết mã dưới dạng GObject hoặc chuyển các cuộc gọi lại xung quanh hoặc sử dụng python-deferđể giúp bạn làm đi. Nhưng tốt nhất là sử dụng Gio ở đây, vì nó có thể mang lại cho bạn nhiều tính năng hay, đặc biệt nếu bạn đang mở tệp / lưu nội dung trong UX.


Cảm ơn @dobey. Tôi thực sự không đọc một tập tin từ đĩa trực tiếp, tôi có lẽ nên làm cho nó rõ ràng hơn trong bài viết gốc. Nhiệm vụ dài hạn mà tôi đang chạy là đọc cơ sở dữ liệu của Trung tâm phần mềm theo câu trả lời cho askubfox.com/questions/139032/ , vì vậy tôi không chắc mình có thể sử dụng GioAPI. Điều tôi băn khoăn là liệu có cách nào để chạy bất kỳ tác vụ chạy chung chung nào một cách không đồng bộ theo cùng cách mà GTask đã từng làm hay không.
David Planella

Tôi không biết chính xác GTask là gì, nhưng nếu bạn muốn nói là gtask.sourceforge.net thì tôi không nghĩ bạn nên sử dụng nó. Nếu đó là một cái gì đó khác, thì tôi không biết nó là gì. Nhưng có vẻ như bạn sẽ phải đi tuyến đường thứ hai mà tôi đã đề cập và triển khai một số API không đồng bộ để bọc mã đó hoặc chỉ thực hiện tất cả trong một chuỗi.
dobey

Có một liên kết đến nó trong câu hỏi. GTask là (đã): chergert.github.com/gtask
David Planella

1
À, nó trông rất giống với API được cung cấp bởi python-defer (và API bị trì hoãn của xoắn). Có lẽ bạn nên xem xét sử dụng python-defer?
dobey

1
Bạn vẫn cần trì hoãn việc được gọi, cho đến sau khi các sự kiện ưu tiên chính đã xảy ra, bằng cách sử dụng GLib.idle_add () chẳng hạn. Như thế này: pastebin.ub
Ubuntu.com/1011660

1

Tôi nghĩ rằng nó lưu ý rằng đây là một cách phức tạp để làm những gì @mhall đề xuất.

Về cơ bản, bạn đã có một hoạt động này sau đó chạy chức năng đó của async_call.

Nếu bạn muốn xem nó hoạt động như thế nào, bạn có thể chơi với bộ hẹn giờ ngủ và tiếp tục nhấp vào nút. Về cơ bản, nó giống như câu trả lời của @ mhall ngoại trừ có mã ví dụ.

Dựa trên điều này không phải là công việc của tôi.

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

Lưu ý thêm, bạn phải để cho chủ đề khác kết thúc trước khi nó kết thúc đúng hoặc kiểm tra một tập tin. Khóa trong chủ đề con của bạn.

Chỉnh sửa để nhận xét địa chỉ:
Ban đầu tôi quênGObject.threads_init() . Rõ ràng khi nút bắn, nó khởi tạo luồng cho tôi. Điều này che dấu lỗi lầm cho tôi.

Nói chung luồng là tạo cửa sổ trong bộ nhớ, ngay lập tức khởi chạy luồng khác, khi luồng hoàn thành cập nhật nút. Tôi đã thêm một giấc ngủ bổ sung trước khi tôi thậm chí gọi cho Gtk.main để xác minh rằng bản cập nhật đầy đủ COULD chạy trước khi cửa sổ được vẽ. Tôi cũng đã nhận xét nó để xác minh rằng việc khởi chạy luồng không cản trở việc vẽ cửa sổ.


1
Cảm ơn. Tôi không chắc mình có thể làm theo nó. Đối với một người, tôi đã dự kiến ​​sẽ slow_loadđược thực thi ngay sau khi giao diện người dùng bắt đầu, nhưng dường như nó không bao giờ được gọi, trừ khi nút được nhấp, điều này làm tôi bối rối một chút, vì tôi nghĩ mục đích của nút chỉ là để cung cấp chỉ dẫn trực quan của trạng thái của nhiệm vụ.
David Planella

Xin lỗi, tôi đã bỏ lỡ một dòng. Điều đó đã làm nó. Tôi quên nói với GObject để sẵn sàng cho chủ đề.
RobotHumans

Nhưng bạn đang gọi vào vòng lặp chính từ một luồng, điều này có thể gây ra sự cố, mặc dù chúng có thể không dễ dàng bị lộ trong ví dụ tầm thường của bạn mà không thực hiện bất kỳ công việc thực tế nào.
dobey

Điểm hợp lệ, nhưng tôi không nghĩ rằng một ví dụ tầm thường đáng để gửi thông báo qua DBus (mà tôi nghĩ rằng một ứng dụng không tầm thường nên làm)
RobotHumans

Hừm, chạy async_calltrong ví dụ này hoạt động với tôi, nhưng nó mang lại sự hỗn loạn khi tôi chuyển nó vào ứng dụng của mình và tôi thêm slow_loadchức năng thực sự tôi có.
David Planella
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.