Los fragmentos de código en esta página necesitan las siguientes adiciones si está fuera de la consola de pyqgis:

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

15. Tareas - haciendo trabajo duro en segundo plano

15.1. Introducción

El procesamiento en segundo plano utilizando subprocesos es una forma de mantener una interfaz de usuario receptiva cuando se está realizando un procesamiento pesado. Las tareas se pueden utilizar para realizar subprocesos en QGIS.

Una tarea (QgsTask) es un contenedor para el código que se realizará en segundo plano, y el administrador de tareas (QgsTaskManager) se usa para controlas la ejecución de las tareas. Estas clases simplifican el procesamiento en segundo plano en QGIS al proporcionar mecanismos de señalización, informes de progreso y acceso al estado de los procesos en segundo plano. Las tareas se pueden agrupar mediante subtareas.

El administrador de tareas global (encontrado con QgsApplication.taskManager()) es usado normalmente. Esto significa que sus tareas pueden no ser las únicas tareas controladas por el administrador de tareas.

Hay varias vías para crear una tarea QGIS:

  • Crea tu propia tarea extendiendo QgsTask

    class SpecialisedTask(QgsTask):
        pass
    
  • Crear una tarea desde una función

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    def heavyFunction():
        # Some CPU intensive processing ...
        pass
    
    def workdone():
        # ... do something useful with the results
        pass
    
    task = QgsTask.fromFunction('heavy function', heavyFunction,
                         onfinished=workdone)
    
  • Crear una tarea desde un algoritmo de procesamiento

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

Advertencia

Cualquier tarea en segundo plano (independientemente de cómo se cree) NUNCA debe usar ningún QObject que viva en el hilo principal, como acceder a QgsVectorLayer, QgsProject o realizar cualquier operación basada en GUI como crear nuevos widgets o interactuar con widgets existentes. Solo se debe acceder o modificar los widgets de Qt desde el hilo principal. Los datos que se utilizan en una tarea se deben copiar antes de iniciar la tarea. Intentar utilizarlos desde subprocesos en segundo plano provocará bloqueos.

Las dependencias entre tareas se pueden describir utilizando la función addSubTask() de QgsTask. Cuando se establece una dependencia, el administrador de tareas determinará automáticamente cómo se ejecutarán estas dependencias. Siempre que sea posible, las dependencias se ejecutarán en paralelo para satisfacerlas lo más rápido posible. Si se cancela una tarea de la que depende otra tarea, la tarea dependiente también se cancelará. Las dependencias circulares pueden hacer posibles los interbloqueos, así que tenga cuidado.

Si una tarea depende de que haya una capa disponible, esto se puede indicar usando la función setDependentLayers() de QgsTask. Si una capa de la que depende una tarea no está disponible, la tarea se cancelará.

Una vez que se ha creado la tarea, se puede programar su ejecución utilizando la función addTask() del administrador de tareas. Agregar una tarea al administrador transfiere automáticamente la propiedad de esa tarea al administrador, y el administrador limpiará y eliminará las tareas después de que se hayan ejecutado. La programación de las tareas está influenciada por la prioridad de la tarea, que se establece en addTask().

El estado de las tareas puede ser monitorizado usando señales y funciones QgsTask y QgsTaskManager.

15.2. Ejemplos

15.2.1. Extendiendo QgsTask

En este ejemplo RandomIntegerSumTask se extiende QgsTask y generará 100 enteros aleatorios entre 0 y 500 durante un período de tiempo específico. Si el número aleatorio es 42, la tarea se cancela y se genera una excepción. Se generan y agregan varias instancias de RandomIntegerSumTask (con subtareas) al administrador de tareas, lo que demuestra dos tipos de dependencias.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
import random
from time import sleep

from qgis.core import (
    QgsApplication, QgsTask, QgsMessageLog,
    )

MESSAGE_CATEGORY = 'RandomIntegerSumTask'

class RandomIntegerSumTask(QgsTask):
    """This shows how to subclass QgsTask"""

    def __init__(self, description, duration):
        super().__init__(description, QgsTask.CanCancel)
        self.duration = duration
        self.total = 0
        self.iterations = 0
        self.exception = None

    def run(self):
        """Here you implement your heavy lifting.
        Should periodically test for isCanceled() to gracefully
        abort.
        This method MUST return True or False.
        Raising exceptions will crash QGIS, so we handle them
        internally and raise them in self.finished
        """
        QgsMessageLog.logMessage('Started task "{}"'.format(
                                     self.description()),
                                 MESSAGE_CATEGORY, Qgis.Info)
        wait_time = self.duration / 100
        for i in range(100):
            sleep(wait_time)
            # use setProgress to report progress
            self.setProgress(i)
            arandominteger = random.randint(0, 500)
            self.total += arandominteger
            self.iterations += 1
            # check isCanceled() to handle cancellation
            if self.isCanceled():
                return False
            # simulate exceptions to show how to abort task
            if arandominteger == 42:
                # DO NOT raise Exception('bad value!')
                # this would crash QGIS
                self.exception = Exception('bad value!')
                return False
        return True

    def finished(self, result):
        """
        This function is automatically called when the task has
        completed (successfully or not).
        You implement finished() to do whatever follow-up stuff
        should happen after the task is complete.
        finished is always called from the main thread, so it's safe
        to do GUI operations and raise Python exceptions here.
        result is the return value from self.run.
        """
        if result:
            QgsMessageLog.logMessage(
                'RandomTask "{name}" completed\n' \
                'RandomTotal: {total} (with {iterations} '\
              'iterations)'.format(
                  name=self.description(),
                  total=self.total,
                  iterations=self.iterations),
              MESSAGE_CATEGORY, Qgis.Success)
        else:
            if self.exception is None:
                QgsMessageLog.logMessage(
                    'RandomTask "{name}" not successful but without '\
                    'exception (probably the task was manually '\
                    'canceled by the user)'.format(
                        name=self.description()),
                    MESSAGE_CATEGORY, Qgis.Warning)
            else:
                QgsMessageLog.logMessage(
                    'RandomTask "{name}" Exception: {exception}'.format(
                        name=self.description(),
                        exception=self.exception),
                    MESSAGE_CATEGORY, Qgis.Critical)
                raise self.exception

    def cancel(self):
        QgsMessageLog.logMessage(
            'RandomTask "{name}" was canceled'.format(
                name=self.description()),
            MESSAGE_CATEGORY, Qgis.Info)
        super().cancel()


longtask = RandomIntegerSumTask('waste cpu long', 20)
shorttask = RandomIntegerSumTask('waste cpu short', 10)
minitask = RandomIntegerSumTask('waste cpu mini', 5)
shortsubtask = RandomIntegerSumTask('waste cpu subtask short', 5)
longsubtask = RandomIntegerSumTask('waste cpu subtask long', 10)
shortestsubtask = RandomIntegerSumTask('waste cpu subtask shortest', 4)

# Add a subtask (shortsubtask) to shorttask that must run after
# minitask and longtask has finished
shorttask.addSubTask(shortsubtask, [minitask, longtask])
# Add a subtask (longsubtask) to longtask that must be run
# before the parent task
longtask.addSubTask(longsubtask, [], QgsTask.ParentDependsOnSubTask)
# Add a subtask (shortestsubtask) to longtask
longtask.addSubTask(shortestsubtask)

QgsApplication.taskManager().addTask(longtask)
QgsApplication.taskManager().addTask(shorttask)
QgsApplication.taskManager().addTask(minitask)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
RandomIntegerSumTask(0): Started task "waste cpu subtask shortest"
RandomIntegerSumTask(0): Started task "waste cpu short"
RandomIntegerSumTask(0): Started task "waste cpu mini"
RandomIntegerSumTask(0): Started task "waste cpu subtask long"
RandomIntegerSumTask(3): Task "waste cpu subtask shortest" completed
RandomTotal: 25452 (with 100 iterations)
RandomIntegerSumTask(3): Task "waste cpu mini" completed
RandomTotal: 23810 (with 100 iterations)
RandomIntegerSumTask(3): Task "waste cpu subtask long" completed
RandomTotal: 26308 (with 100 iterations)
RandomIntegerSumTask(0): Started task "waste cpu long"
RandomIntegerSumTask(3): Task "waste cpu long" completed
RandomTotal: 22534 (with 100 iterations)

15.2.2. Tarea desde función

Cree una tarea desde una función (doSomething en este ejemplo). El primer parámetro de la función contendrá el QgsTask para la función. Un importante parámetro(llamado) es on_finished, que especifica una función que se llamará cuando la tarea se haya completado. La función doSomething en este ejemplo tiene un parámetro adicional llamado wait_time.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import random
from time import sleep

MESSAGE_CATEGORY = 'TaskFromFunction'

def doSomething(task, wait_time):
    """
    Raises an exception to abort the task.
    Returns a result if success.
    The result will be passed, together with the exception (None in
    the case of success), to the on_finished method.
    If there is an exception, there will be no result.
    """
    QgsMessageLog.logMessage('Started task {}'.format(task.description()),
                             MESSAGE_CATEGORY, Qgis.Info)
    wait_time = wait_time / 100
    total = 0
    iterations = 0
    for i in range(100):
        sleep(wait_time)
        # use task.setProgress to report progress
        task.setProgress(i)
        arandominteger = random.randint(0, 500)
        total += arandominteger
        iterations += 1
        # check task.isCanceled() to handle cancellation
        if task.isCanceled():
            stopped(task)
            return None
        # raise an exception to abort the task
        if arandominteger == 42:
            raise Exception('bad value!')
    return {'total': total, 'iterations': iterations,
            'task': task.description()}

def stopped(task):
    QgsMessageLog.logMessage(
        'Task "{name}" was canceled'.format(
            name=task.description()),
        MESSAGE_CATEGORY, Qgis.Info)

def completed(exception, result=None):
    """This is called when doSomething is finished.
    Exception is not None if doSomething raises an exception.
    result is the return value of doSomething."""
    if exception is None:
        if result is None:
            QgsMessageLog.logMessage(
                'Completed with no exception and no result '\
                '(probably manually canceled by the user)',
                MESSAGE_CATEGORY, Qgis.Warning)
        else:
            QgsMessageLog.logMessage(
                'Task {name} completed\n'
                'Total: {total} ( with {iterations} '
                'iterations)'.format(
                    name=result['task'],
                    total=result['total'],
                    iterations=result['iterations']),
                MESSAGE_CATEGORY, Qgis.Info)
    else:
        QgsMessageLog.logMessage("Exception: {}".format(exception),
                                 MESSAGE_CATEGORY, Qgis.Critical)
        raise exception

# Create a few tasks
task1 = QgsTask.fromFunction('Waste cpu 1', doSomething,
                             on_finished=completed, wait_time=4)
task2 = QgsTask.fromFunction('Waste cpu 2', doSomething,
                             on_finished=completed, wait_time=3)
QgsApplication.taskManager().addTask(task1)
QgsApplication.taskManager().addTask(task2)
1
2
3
4
5
6
7
RandomIntegerSumTask(0): Started task "waste cpu subtask short"
RandomTaskFromFunction(0): Started task Waste cpu 1
RandomTaskFromFunction(0): Started task Waste cpu 2
RandomTaskFromFunction(0): Task Waste cpu 2 completed
RandomTotal: 23263 ( with 100 iterations)
RandomTaskFromFunction(0): Task Waste cpu 1 completed
RandomTotal: 25044 ( with 100 iterations)

15.2.3. Tarea de un algoritmo de procesamiento

Crear una tarea que use el algoritmo qgis:randompointsinextent para generar 50000 puntos aleatorios dentro de una extensión específica. El resultado se agrega al proyecto de manera segura.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from functools import partial
from qgis.core import (QgsTaskManager, QgsMessageLog,
                       QgsProcessingAlgRunnerTask, QgsApplication,
                       QgsProcessingContext, QgsProcessingFeedback,
                       QgsProject)

MESSAGE_CATEGORY = 'AlgRunnerTask'

def task_finished(context, successful, results):
    if not successful:
        QgsMessageLog.logMessage('Task finished unsucessfully',
                                 MESSAGE_CATEGORY, Qgis.Warning)
    output_layer = context.getMapLayer(results['OUTPUT'])
    # because getMapLayer doesn't transfer ownership, the layer will
    # be deleted when context goes out of scope and you'll get a
    # crash.
    # takeMapLayer transfers ownership so it's then safe to add it
    # to the project and give the project ownership.
    if output_layer and output_layer.isValid():
        QgsProject.instance().addMapLayer(
             context.takeResultLayer(output_layer.id()))

alg = QgsApplication.processingRegistry().algorithmById(
                                      'qgis:randompointsinextent')
context = QgsProcessingContext()
feedback = QgsProcessingFeedback()
params = {
    'EXTENT': '0.0,10.0,40,50 [EPSG:4326]',
    'MIN_DISTANCE': 0.0,
    'POINTS_NUMBER': 50000,
    'TARGET_CRS': 'EPSG:4326',
    'OUTPUT': 'memory:My random points'
}
task = QgsProcessingAlgRunnerTask(alg, params, context, feedback)
task.executed.connect(partial(task_finished, context))
QgsApplication.taskManager().addTask(task)

Ver también: https://opengis.ch/2018/06/22/threads-in-pyqgis3/.