De codesnippers op deze pagina hebben de volgende import nodig als u buiten de console van PyQGIS bent:

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

15. Taken - veel werk op de achtergrond doen

15.1. Introductie

Verwerken op de achtergrond met behulp van threads is een manier om een reagerende gebruikersinterface te behouden wanneer veel verwerking wordt uitgevoerd. Taken kunnen worden gebruikt om threading te gebruiken in QGIS.

Een taak (QgsTask) is een container voor de code om op de achtergrond te wordne uitgevoerd, en de taakbeheerder (QgsTaskManager) wordt gebruikt om het uitvoeren van de taken te beheren. Deze klassen vereenvoudigen het verwken op de achtergrond in QGIS door mechanismen te verschaffen voor signaleren, voortgang rapporteren en toegang tot de status voor processen op de achtergrond. Taken kunnen worden gegroepeerd met behulp van subtaken.

Normaal gesproken wordt de globale taakbeheerder (te vinden met QgsApplication.taskManager()). Dit betekent dat uw taken niet de enige taken zijn die worden beheerd door de taakbeheerder.

Er zijn verschillende manieren om een taak voor QGIS te maken:

  • Maak uw eigen taak door QgsTask uit te breiden

    class SpecialisedTask(QgsTask):
        pass
    
  • Maak een taak uit een functie.

     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)
    
  • Maak een taak uit een algoritme van Processing.

    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)
    

Waarschuwing

Elke taak op de achtergrond (ongeacht hoe die is gemaakt) moet NOOIT een QObject gebruiken dat leeft in de hoofdthread, zoals toegang tot QgsVectorLayer, QgsProject of op de GUI gebaseerde bewerkingen uitvoeren, zoals het maken van nieuwe widgets of interactief zijn met bestaande widgets. Toegang tot of aanpassen van widgets voor Qt moet alleen gebeuren vanuit de hoofdthread. Gegevens die worden gebruikt in een taak moeten zijn gekopieerd voordat de taak wordt gestart. Pogingen om ze te gebruiken vanuit threads voor de achtergrond zal leiden tot crashes.

Afhankelijkheden tussen taken kunnen worden beschreven met de functie addSubTask() van QgsTask. Wanneer een afhankelijkheid wordt aangegeven,zal de taakbeheerder automatisch bepalen doe die afhankelijkheden zullen worden uitgevoerd. Waar mogelijk worden afhankelijkheden parallel uitgevoerd om ze zo snel als mogelijk voltooid te krijgen. Indien een taak waarvan een andere taak afhankelijk is wordt geannuleerd, zal de afhankelijke taak ook worden geannuleerd. Circulaire afhankelijkheden kunnen vastlopers mogelijk maken, wees dus voorzichtig.

Of een taak afhankelijk is van het feit of een laag beschikbaar is kan worden aangegeven met behulp van de functie setDependentLayers() van QgsTask. Indien een laag, waarvan de taak afhankelijk is, niet beschikbaar is, wordt de taak geannuleerd.

Als de taak eenmaal is gemaakt kan die voor uitvoering in een schema worden geplaatst met behulp van de functie addTask() van de taakbeheerder. Toevoegen van een taak aan de beheerder draagt automatisch het eigendom van die taak over aan de beheerder en de beheerder zal taken opschonen en verwijderen nadat zij zijn uitgevoerd. Het in schema zetten van de taken wordt beïnvloed door de prioriteit van de taak, die wordt ingesteld in addTask().

De status van taken kan worden gemonitord met behulp van signalen en functies van QgsTask en QgsTaskManager.

15.2. Voorbeelden

15.2.1. QgsTask uitbreiden

In dit voorbeeld breidt RandomIntegerSumTask QgsTask uit en zal 100 willekeurige integers tussen 0 en 500 genereren gedurende een gespecificeerde tijdperiode. Als het willekeurige getal 42 is zal de taak worden afgebroken en een uitzondering opgeworpen. Verscheidene instances van RandomIntegerSumTask (met subtaken) worden gemaakt en toegevoegd aan de taakbeheerder, wat twee typen afhankelijkheden demonstreert.

  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. Taak uit functie

Maak een taak uit een functie (in dit voorbeeld doSomething). De eerste parameter van de functie zal de klasse QgsTask voor de functie bevatten. Een belangrijke (benoemde) parameter is on_finished, die een functie specificeert die zal worden aangeroepen als de taak is voltooid. De functie doSomething in dit voorbeeld heeft een aanvullende benoemde parameter 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. Taak uit een algoritme voor Processing

Maak een taak die het algoritme qgis:randompointsinextent gebruikt om 50000 willekeurige punten te maken in een gespecificeerd bereik. Het resultaat wordt op een veilige manier aan het project toegevoegd.

 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)

Bekijk ook: https://opengis.ch/2018/06/22/threads-in-pyqgis3/.