15. المهام - تنفيذ الأعمال الثقيلة في الخلفية

تلميح

تحتاج مقتطفات الشيفرة في هذه الصفحة إلى عمليات الاستيراد التالية إذا كنت خارج وحدة تحكم pyqgis:

 1from qgis.core import (
 2  Qgis,
 3  QgsApplication,
 4  QgsMessageLog,
 5  QgsProcessingAlgRunnerTask,
 6  QgsProcessingContext,
 7  QgsProcessingFeedback,
 8  QgsProject,
 9  QgsTask,
10  QgsTaskManager,
11)

15.1. المقدمة

المعالجة في الخلفية باستخدام الخيوط (threads) وسيلة للحفاظ على واجهة مستخدم سريعة الاستجابة أثناء إجراء معالجة ثقيلة. ويمكن استخدام المهام لتحقيق تعدد الخيوط في QGIS.

المهمة (QgsTask) حاوية للشيفرة المراد تنفيذها في الخلفية، ويُستخدم مدير المهام (QgsTaskManager) للتحكم في تشغيل المهام. وتبسّط هذه الأصناف المعالجة في الخلفية في QGIS بتوفير آليات للإشارات والإبلاغ عن التقدم والوصول إلى حالة العمليات الجارية في الخلفية. ويمكن تجميع المهام باستخدام المهام الفرعية.

يُستخدم عادةً مدير المهام العام (الذي تحصل عليه بـ QgsApplication.taskManager()). وهذا يعني أن مهامك قد لا تكون المهام الوحيدة التي يتحكم فيها مدير المهام.

هناك عدة طرق لإنشاء مهمة QGIS:

  • أنشئ مهمتك الخاصة بتوسيع QgsTask

    class SpecialisedTask(QgsTask):
        pass
    
  • إنشاء مهمة من دالة

     1def heavyFunction():
     2    # Some CPU intensive processing ...
     3    pass
     4
     5def workdone():
     6    # ... do something useful with the results
     7    pass
     8
     9task = QgsTask.fromFunction('heavy function', heavyFunction,
    10                     on_finished=workdone)
    
  • إنشاء مهمة من خوارزمية معالجة

    1params = dict()
    2context = QgsProcessingContext()
    3context.setProject(QgsProject.instance())
    4feedback = QgsProcessingFeedback()
    5
    6buffer_alg = QgsApplication.instance().processingRegistry().algorithmById('native:buffer')
    7task = QgsProcessingAlgRunnerTask(buffer_alg, params, context,
    8                           feedback)
    

تحذير

يجب على أي مهمة تعمل في الخلفية (بغض النظر عن طريقة إنشائها) ألّا تستخدم إطلاقًا أي QObject يعيش في الخيط الرئيسي، مثل الوصول إلى QgsVectorLayer أو QgsProject، وألّا تُجري أي عمليات قائمة على واجهة المستخدم الرسومية مثل إنشاء عناصر واجهة جديدة أو التفاعل مع عناصر واجهة موجودة. فلا يجوز الوصول إلى عناصر واجهة Qt أو تعديلها إلا من الخيط الرئيسي. ويجب نسخ البيانات المستخدمة في المهمة قبل بدء المهمة. ومحاولة استخدامها من خيوط الخلفية ستؤدي إلى انهيار البرنامج.

علاوة على ذلك، تأكد دائمًا من أن context و feedback يبقيان موجودين طوال مدة بقاء المهام التي تستخدمهما على الأقل. فسينهار QGIS إذا فشل QgsTaskManager، عند اكتمال المهمة، في الوصول إلى context و feedback اللذين جُدولت المهمة بناءً عليهما.

ملاحظة

من الأنماط الشائعة استدعاء setProject() بعد استدعاء QgsProcessingContext بوقت قصير. فهذا يتيح للمهمة ولدالة الاستدعاء الراجع الخاصة بها استخدام معظم إعدادات المشروع. وهذا مفيد بشكل خاص عند العمل مع الطبقات المكانية في دالة الاستدعاء الراجع.

يمكن وصف الاعتماديات بين المهام باستخدام الدالة addSubTask() من QgsTask. وعند تحديد اعتمادية، سيحدد مدير المهام تلقائيًا كيفية تنفيذ هذه الاعتماديات. وحيثما أمكن، ستُنفَّذ الاعتماديات بالتوازي لتلبيتها بأسرع ما يمكن. وإذا أُلغيت مهمة تعتمد عليها مهمة أخرى، فستُلغى المهمة المعتمِدة أيضًا. وقد تؤدي الاعتماديات الدائرية إلى حالات جمود (deadlocks)، فكن حذرًا.

إذا كانت مهمة ما تعتمد على توفر طبقة، فيمكن تحديد ذلك باستخدام الدالة setDependentLayers() من QgsTask. وإذا لم تكن الطبقة التي تعتمد عليها المهمة متاحة، فستُلغى المهمة.

بعد إنشاء المهمة يمكن جدولتها للتشغيل باستخدام الدالة addTask() من مدير المهام. وإضافة مهمة إلى المدير تنقل ملكية تلك المهمة إلى المدير تلقائيًا، وسيتولى المدير تنظيف المهام وحذفها بعد تنفيذها. وتتأثر جدولة المهام بأولوية المهمة، التي تُحدَّد في addTask().

يمكن مراقبة حالة المهام باستخدام إشارات ودوال QgsTask و QgsTaskManager.

15.2. أمثلة

15.2.1. توسيع QgsTask

في هذا المثال، يوسّع RandomIntegerSumTask الصنف QgsTask وسيولّد 100 عدد صحيح عشوائي بين 0 و500 خلال مدة زمنية محددة. وإذا كان العدد العشوائي 42، تُجهَض المهمة ويُطلق استثناء. وتُنشأ عدة نسخ من RandomIntegerSumTask (مع مهام فرعية) وتُضاف إلى مدير المهام، لتوضيح نوعين من الاعتماديات.

  1import random
  2from time import sleep
  3
  4from qgis.core import (
  5    QgsApplication, QgsTask, QgsMessageLog, Qgis
  6    )
  7
  8MESSAGE_CATEGORY = 'RandomIntegerSumTask'
  9
 10class RandomIntegerSumTask(QgsTask):
 11    """This shows how to subclass QgsTask"""
 12
 13    def __init__(self, description, duration):
 14        super().__init__(description, QgsTask.CanCancel)
 15        self.duration = duration
 16        self.total = 0
 17        self.iterations = 0
 18        self.exception = None
 19
 20    def run(self):
 21        """Here you implement your heavy lifting.
 22        Should periodically test for isCanceled() to gracefully
 23        abort.
 24        This method MUST return True or False.
 25        Raising exceptions will crash QGIS, so we handle them
 26        internally and raise them in self.finished
 27        """
 28        QgsMessageLog.logMessage('Started task "{}"'.format(
 29                                     self.description()),
 30                                 MESSAGE_CATEGORY, Qgis.Info)
 31        wait_time = self.duration / 100
 32        for i in range(100):
 33            sleep(wait_time)
 34            # use setProgress to report progress
 35            self.setProgress(i)
 36            arandominteger = random.randint(0, 500)
 37            self.total += arandominteger
 38            self.iterations += 1
 39            # check isCanceled() to handle cancellation
 40            if self.isCanceled():
 41                return False
 42            # simulate exceptions to show how to abort task
 43            if arandominteger == 42:
 44                # DO NOT raise Exception('bad value!')
 45                # this would crash QGIS
 46                self.exception = Exception('bad value!')
 47                return False
 48        return True
 49
 50    def finished(self, result):
 51        """
 52        This function is automatically called when the task has
 53        completed (successfully or not).
 54        You implement finished() to do whatever follow-up stuff
 55        should happen after the task is complete.
 56        finished is always called from the main thread, so it's safe
 57        to do GUI operations and raise Python exceptions here.
 58        result is the return value from self.run.
 59        """
 60        if result:
 61            QgsMessageLog.logMessage(
 62                'RandomTask "{name}" completed\n' \
 63                'RandomTotal: {total} (with {iterations} '\
 64              'iterations)'.format(
 65                  name=self.description(),
 66                  total=self.total,
 67                  iterations=self.iterations),
 68              MESSAGE_CATEGORY, Qgis.Success)
 69        else:
 70            if self.exception is None:
 71                QgsMessageLog.logMessage(
 72                    'RandomTask "{name}" not successful but without '\
 73                    'exception (probably the task was manually '\
 74                    'canceled by the user)'.format(
 75                        name=self.description()),
 76                    MESSAGE_CATEGORY, Qgis.Warning)
 77            else:
 78                QgsMessageLog.logMessage(
 79                    'RandomTask "{name}" Exception: {exception}'.format(
 80                        name=self.description(),
 81                        exception=self.exception),
 82                    MESSAGE_CATEGORY, Qgis.Critical)
 83                raise self.exception
 84
 85    def cancel(self):
 86        QgsMessageLog.logMessage(
 87            'RandomTask "{name}" was canceled'.format(
 88                name=self.description()),
 89            MESSAGE_CATEGORY, Qgis.Info)
 90        super().cancel()
 91
 92
 93longtask = RandomIntegerSumTask('waste cpu long', 20)
 94shorttask = RandomIntegerSumTask('waste cpu short', 10)
 95minitask = RandomIntegerSumTask('waste cpu mini', 5)
 96shortsubtask = RandomIntegerSumTask('waste cpu subtask short', 5)
 97longsubtask = RandomIntegerSumTask('waste cpu subtask long', 10)
 98shortestsubtask = RandomIntegerSumTask('waste cpu subtask shortest', 4)
 99
100# Add a subtask (shortsubtask) to shorttask that must run after
101# minitask and longtask has finished
102shorttask.addSubTask(shortsubtask, [minitask, longtask])
103# Add a subtask (longsubtask) to longtask that must be run
104# before the parent task
105longtask.addSubTask(longsubtask, [], QgsTask.ParentDependsOnSubTask)
106# Add a subtask (shortestsubtask) to longtask
107longtask.addSubTask(shortestsubtask)
108
109QgsApplication.taskManager().addTask(longtask)
110QgsApplication.taskManager().addTask(shorttask)
111QgsApplication.taskManager().addTask(minitask)
 1RandomIntegerSumTask(0): Started task "waste cpu subtask shortest"
 2RandomIntegerSumTask(0): Started task "waste cpu short"
 3RandomIntegerSumTask(0): Started task "waste cpu mini"
 4RandomIntegerSumTask(0): Started task "waste cpu subtask long"
 5RandomIntegerSumTask(3): Task "waste cpu subtask shortest" completed
 6RandomTotal: 25452 (with 100 iterations)
 7RandomIntegerSumTask(3): Task "waste cpu mini" completed
 8RandomTotal: 23810 (with 100 iterations)
 9RandomIntegerSumTask(3): Task "waste cpu subtask long" completed
10RandomTotal: 26308 (with 100 iterations)
11RandomIntegerSumTask(0): Started task "waste cpu long"
12RandomIntegerSumTask(3): Task "waste cpu long" completed
13RandomTotal: 22534 (with 100 iterations)

15.2.2. مهمة من دالة

أنشئ مهمة من دالة (doSomething في هذا المثال). سيحمل المعامل الأول للدالة QgsTask الخاص بالدالة. ومن المعاملات (المسمّاة) المهمة on_finished، الذي يحدد دالة تُستدعى عند اكتمال المهمة. وللدالة doSomething في هذا المثال معامل مسمّى إضافي هو wait_time.

 1import random
 2from time import sleep
 3
 4MESSAGE_CATEGORY = 'TaskFromFunction'
 5
 6def doSomething(task, wait_time):
 7    """
 8    Raises an exception to abort the task.
 9    Returns a result if success.
10    The result will be passed, together with the exception (None in
11    the case of success), to the on_finished method.
12    If there is an exception, there will be no result.
13    """
14    QgsMessageLog.logMessage('Started task {}'.format(task.description()),
15                             MESSAGE_CATEGORY, Qgis.Info)
16    wait_time = wait_time / 100
17    total = 0
18    iterations = 0
19    for i in range(100):
20        sleep(wait_time)
21        # use task.setProgress to report progress
22        task.setProgress(i)
23        arandominteger = random.randint(0, 500)
24        total += arandominteger
25        iterations += 1
26        # check task.isCanceled() to handle cancellation
27        if task.isCanceled():
28            stopped(task)
29            return None
30        # raise an exception to abort the task
31        if arandominteger == 42:
32            raise Exception('bad value!')
33    return {'total': total, 'iterations': iterations,
34            'task': task.description()}
35
36def stopped(task):
37    QgsMessageLog.logMessage(
38        'Task "{name}" was canceled'.format(
39            name=task.description()),
40        MESSAGE_CATEGORY, Qgis.Info)
41
42def completed(exception, result=None):
43    """This is called when doSomething is finished.
44    Exception is not None if doSomething raises an exception.
45    result is the return value of doSomething."""
46    if exception is None:
47        if result is None:
48            QgsMessageLog.logMessage(
49                'Completed with no exception and no result '\
50                '(probably manually canceled by the user)',
51                MESSAGE_CATEGORY, Qgis.Warning)
52        else:
53            QgsMessageLog.logMessage(
54                'Task {name} completed\n'
55                'Total: {total} ( with {iterations} '
56                'iterations)'.format(
57                    name=result['task'],
58                    total=result['total'],
59                    iterations=result['iterations']),
60                MESSAGE_CATEGORY, Qgis.Info)
61    else:
62        QgsMessageLog.logMessage("Exception: {}".format(exception),
63                                 MESSAGE_CATEGORY, Qgis.Critical)
64        raise exception
65
66# Create a few tasks
67task1 = QgsTask.fromFunction('Waste cpu 1', doSomething,
68                             on_finished=completed, wait_time=4)
69task2 = QgsTask.fromFunction('Waste cpu 2', doSomething,
70                             on_finished=completed, wait_time=3)
71QgsApplication.taskManager().addTask(task1)
72QgsApplication.taskManager().addTask(task2)
1RandomIntegerSumTask(0): Started task "waste cpu subtask short"
2RandomTaskFromFunction(0): Started task Waste cpu 1
3RandomTaskFromFunction(0): Started task Waste cpu 2
4RandomTaskFromFunction(0): Task Waste cpu 2 completed
5RandomTotal: 23263 ( with 100 iterations)
6RandomTaskFromFunction(0): Task Waste cpu 1 completed
7RandomTotal: 25044 ( with 100 iterations)

15.2.3. مهمة من خوارزمية معالجة

أنشئ مهمة تستخدم الخوارزمية qgis:randompointsinextent لتوليد 50000 نقطة عشوائية داخل امتداد محدد. وتُضاف النتيجة إلى المشروع بطريقة آمنة.

 1from functools import partial
 2from qgis.core import (QgsTaskManager, QgsMessageLog,
 3                       QgsProcessingAlgRunnerTask, QgsApplication,
 4                       QgsProcessingContext, QgsProcessingFeedback,
 5                       QgsProject)
 6
 7MESSAGE_CATEGORY = 'AlgRunnerTask'
 8
 9def task_finished(context, successful, results):
10    if not successful:
11        QgsMessageLog.logMessage('Task finished unsucessfully',
12                                 MESSAGE_CATEGORY, Qgis.Warning)
13    output_layer = context.getMapLayer(results['OUTPUT'])
14    # because getMapLayer doesn't transfer ownership, the layer will
15    # be deleted when context goes out of scope and you'll get a
16    # crash.
17    # takeMapLayer transfers ownership so it's then safe to add it
18    # to the project and give the project ownership.
19    if output_layer and output_layer.isValid():
20        QgsProject.instance().addMapLayer(
21             context.takeResultLayer(output_layer.id()))
22
23alg = QgsApplication.processingRegistry().algorithmById(
24                                      'qgis:randompointsinextent')
25# `context` and `feedback` need to
26# live for as least as long as `task`,
27# otherwise the program will crash.
28# Initializing them globally is a sure way
29# of avoiding this unfortunate situation.
30context = QgsProcessingContext()
31feedback = QgsProcessingFeedback()
32params = {
33    'EXTENT': '0.0,10.0,40,50 [EPSG:4326]',
34    'MIN_DISTANCE': 0.0,
35    'POINTS_NUMBER': 50000,
36    'TARGET_CRS': 'EPSG:4326',
37    'OUTPUT': 'memory:My random points'
38}
39task = QgsProcessingAlgRunnerTask(alg, params, context, feedback)
40task.executed.connect(partial(task_finished, context))
41QgsApplication.taskManager().addTask(task)

انظر أيضًا: https://www.opengis.ch/2018/06/22/threads-in-pyqgis3/.