Les extraits de code sur cette page nécessitent les importations suivantes si vous êtes en dehors de la console pyqgis :

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

15. Tâches - faire un gros travail en arrière-plan

15.1. Introduction

Le traitement en arrière-plan à l’aide de threads est un moyen de maintenir une interface utilisateur réactive en cas de traitement lourd. Les tâches peuvent être utilisées pour réaliser des threads dans QGIS.

Une tâche (QgsTaskManager) est un conteneur pour le code à exécuter en arrière-plan, et le gestionnaire de tâches (QgsTaskManager) est utilisé pour contrôler l’exécution des tâches. Ces classes simplifient le traitement en arrière-plan dans QGIS en fournissant des mécanismes de signalisation, de rapport d’avancement et d’accès à l’état des processus en arrière-plan. Les tâches peuvent être regroupées à l’aide de sous-tâches.

Le gestionnaire de tâches global (trouvé avec QgsApplication.taskManager()) est normalement utilisé. Cela signifie que vos tâches peuvent ne pas être les seules à être contrôlées par le gestionnaire de tâches.

Il existe plusieurs façons de créer une tâche QGIS :

  • Créez votre propre tâche en étendant QgsTask.

    class SpecialisedTask(QgsTask):
        pass
    
  • Créer une tâche à partir d’une fonction

     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)
    
  • Créer une tâche à partir d’un algorithme de traitement

    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)
    

Avertissement

Toute tâche en arrière-plan (quelle que soit la façon dont elle est créée) ne doit JAMAIS utiliser un QObject qui vit sur le thread principal, comme l’accès à QgsVectorLayer, QgsProject ou effectuer des opérations basées sur une interface graphique comme la création de nouveaux widgets ou l’interaction avec des widgets existants. Les widgets Qt ne doivent être accessibles ou modifiés que depuis le fil principal. Les données utilisées dans une tâche doivent être copiées avant que la tâche ne soit lancée. Tenter de les utiliser à partir des fils de discussion en arrière-plan entraînera des plantages.

Les dépendances entre les tâches peuvent être décrites en utilisant la fonction addSubTask de QgsTask. Lorsqu’une dépendance est indiquée, le gestionnaire de tâches détermine automatiquement comment ces dépendances seront exécutées. Dans la mesure du possible, les dépendances seront exécutées en parallèle afin de les satisfaire le plus rapidement possible. Si une tâche dont dépend une autre tâche est annulée, la tâche dépendante sera également annulée. Les dépendances circulaires peuvent rendre possible des blocages, soyez donc prudent.

Si une tâche dépend de la disponibilité d’une couche, cela peut être indiqué en utilisant la fonction setDependentLayers de QgsTask. Si une couche dont dépend une tâche n’est pas disponible, la tâche sera annulée.

Une fois que la tâche a été créée, elle peut être programmée pour s’exécuter en utilisant la fonction addTask du gestionnaire de tâches. L’ajout d’une tâche au gestionnaire transfère automatiquement la propriété de cette tâche au gestionnaire, et le gestionnaire nettoiera et supprimera les tâches après leur exécution. La planification des tâches est influencée par la priorité des tâches, qui est définie dans addTask.

L’état des tâches peut être surveillé en utilisant les signaux et fonctions QgsTask et QgsTaskManager.

15.2. Exemples

15.2.1. Extension de QgsTask

Dans cet exemple, RandomIntegerSumTask étend QgsTask et va générer 100 entiers aléatoires entre 0 et 500 pendant une période de temps spécifiée. Si le nombre aléatoire est de 42, la tâche est abandonnée et une exception est levée. Plusieurs instances de RandomIntegerSumTask (avec des sous-tâches) sont générées et ajoutées au gestionnaire de tâches, démontrant ainsi deux types de dépendances.

  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. Tâche de la fonction

Créer une tâche à partir d’une fonction (doSomething dans cet exemple). Le premier paramètre de la fonction contiendra la QgsTask pour la fonction. Un paramètre important (nommé) est on_finished, qui spécifie une fonction qui sera appelée lorsque la tâche sera terminée. La fonction doSomething dans cet exemple a un paramètre supplémentaire nommé 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. Tâche à partir d’un algorithme de traitement

Créer une tâche qui utilise l’algorithme qgis:randompointsinextent pour générer 50000 points aléatoires à l’intérieur d’une étendue spécifiée. Le résultat est ajouté au projet de manière sûre.

 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)

Voir également : https://opengis.ch/2018/06/22/threads-in-pyqgis3/.