15. Task - fare un lavoro pesante in background

Suggerimento

I frammenti di codice di questa pagina necessitano delle seguenti importazioni se sei è al di fuori della console pyqgis:

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

15.1. Introduzione

L’elaborazione in background tramite thread è un modo per mantenere un’interfaccia utente reattiva quando è in corso un’elaborazione pesante. I task possono essere utilizzati per ottenere il threading in QGIS.

Un task (QgsTask) è un contenitore per il codice da eseguire in background e il task manager (QgsTaskManager) è usato per controllare l’esecuzione dei task. Queste classi semplificano l’elaborazione in background in QGIS, fornendo meccanismi per la segnalazione, l’avanzamento e l’accesso allo stato dei processi in background. I task possono essere raggruppati utilizzando i sottotask.

Normalmente viene utilizzato il task manager globale (trovato con QgsApplication.taskManager()). Ciò significa che i tuoi task potrebbero non essere gli unici controllati dal task manager.

Esistono diversi modi per creare un task QGIS:

  • Creare una tua attività personalizzata estendendo QgsTask

    class SpecialisedTask(QgsTask):
        pass
    
  • Creare un task da una funzione

     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                     onfinished=workdone)
    
  • Creare un task da un algoritmo di processing

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

Avvertimento

Qualsiasi attività in background (indipendentemente da come è stata creata) non deve MAI utilizzare alcun QObject che vive sul thread principale, come ad esempio accedere a QgsVectorLayer, QgsProject o eseguire operazioni basate sull’interfaccia grafica, come la creazione di nuovi widget o l’interazione con i widget esistenti. I widget Qt devono essere accessibili o modificati solo dal thread principale. I dati utilizzati in un’attività devono essere copiati prima dell’avvio dell’attività stessa. Il tentativo di utilizzarli da thread in background provocherà un arresto anomalo.

Le dipendenze tra attività possono essere descritte utilizzando la funzione addSubTask() di QgsTask. Quando viene dichiarata una dipendenza, il task manager determina automaticamente il modo in cui queste dipendenze saranno eseguite. Ove possibile, le dipendenze saranno eseguite in parallelo, in modo da soddisfarle il più rapidamente possibile. Se un task da cui dipende un altro task viene annullato, anche il task dipendente verrà annullato. Le dipendenze circolari possono rendere possibili i deadlock, quindi bisogna fare attenzione.

Se un’attività dipende dalla disponibilità di un layer, questo può essere dichiarato utilizzando la funzione setDependentLayers() della QgsTask. Se un layer da cui dipende un task non è disponibile, il task viene annullato.

Una volta creato, il task può essere pianificato per l’esecuzione utilizzando la funzione addTask() del task manager. L’aggiunta di un task al gestore ne trasferisce automaticamente la proprietà al gestore stesso, che provvederà a ripulire e cancellare i task dopo la loro esecuzione. La programmazione dei task è influenzata dalla priorità del task, impostata in addTask().

Lo stato delle attività può essere monitorato utilizzando i servizi e le funzioni QgsTask e QgsTaskManager.

15.2. Esempi

15.2.1. Estensione di QgsTask

In questo esempio RandomIntegerSumTask estende QgsTask e genererà 100 numeri interi casuali tra 0 e 500 durante un periodo di tempo specificato. Se il numero casuale è 42, il task viene interrotto e viene sollevata un’eccezione. Diverse istanze di RandomIntegerSumTask (con sottoattività) vengono generate e aggiunte al task manager, dimostrando due tipi di dipendenze.

  1import random
  2from time import sleep
  3
  4from qgis.core import (
  5    QgsApplication, QgsTask, QgsMessageLog,
  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. Task da funzione

Crea un task da una funzione (doSomething in questo esempio). Il primo parametro della funzione conterrà la QgsTask per la funzione. Un parametro importante (con nome) è on_finished, che specifica una funzione che sarà chiamata quando il task è stato completato. La funzione doSomething in questo esempio ha un parametro aggiuntivo chiamato 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. Task da un algoritmo di processing

Crea un task che utilizza l’algoritmo qgis:randompointsinextent per generare 50000 punti casuali all’interno di un’estensione specificata. Il risultato viene aggiunto al progetto in modo sicuro.

 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')
25context = QgsProcessingContext()
26feedback = QgsProcessingFeedback()
27params = {
28    'EXTENT': '0.0,10.0,40,50 [EPSG:4326]',
29    'MIN_DISTANCE': 0.0,
30    'POINTS_NUMBER': 50000,
31    'TARGET_CRS': 'EPSG:4326',
32    'OUTPUT': 'memory:My random points'
33}
34task = QgsProcessingAlgRunnerTask(alg, params, context, feedback)
35task.executed.connect(partial(task_finished, context))
36QgsApplication.taskManager().addTask(task)

vedi anche: https://opengis.ch/2018/06/22/threads-in-pyqgis3/.