Viktigt

Översättning är en gemenskapsinsats du kan gå med i. Den här sidan är för närvarande översatt till 100.00%.

15. Tasks - utför tungt arbete i bakgrunden

Råd

Kodsnuttarna på den här sidan behöver följande import om du befinner dig utanför pyqgis-konsolen:

 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. Introduktion

Bakgrundsbearbetning med hjälp av trådar är ett sätt att bibehålla ett responsivt användargränssnitt när tung bearbetning pågår. Tasks kan användas för att uppnå trådning i QGIS.

En uppgift (QgsTask) är en behållare för den kod som ska utföras i bakgrunden, och uppgiftshanteraren (QgsTaskManager) används för att styra körningen av uppgifterna. Dessa klasser förenklar bakgrundsbearbetningen i QGIS genom att tillhandahålla mekanismer för signalering, förloppsrapportering och åtkomst till status för bakgrundsprocesser. Uppgifter kan grupperas med hjälp av underuppgifter.

Den globala uppgiftshanteraren (hittas med QgsApplication.taskManager()) används normalt. Detta innebär att dina uppgifter kanske inte är de enda uppgifter som styrs av uppgiftshanteraren.

Det finns flera sätt att skapa en QGIS-uppgift:

  • Skapa din egen uppgift genom att utöka QgsTask

    class SpecialisedTask(QgsTask):
        pass
    
  • Skapa en uppgift från en funktion

     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)
    
  • Skapa en uppgift från en bearbetningsalgoritm

    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)
    

Varning

En bakgrundsuppgift (oavsett hur den skapas) får ALDRIG använda något QObject som finns i huvudtråden, t.ex. QgsVectorLayer, QgsProject eller utföra GUI-baserade operationer som att skapa nya widgetar eller interagera med befintliga widgetar. Qt-widgetar får endast nås eller ändras från huvudtråden. Data som används i en uppgift måste kopieras innan uppgiften startas. Försök att använda dem från bakgrundstrådar kommer att leda till krascher.

Se dessutom alltid till att context och feedback lever minst lika länge som de uppgifter som använder dem. QGIS kommer att krascha om QgsTaskManager, när en uppgift har slutförts, misslyckas med att komma åt kontexten och feedback som uppgiften schemalades mot.

Observera

Det är ett vanligt mönster att anropa setProject() strax efter anropet av QgsProcessingContext. Detta gör att uppgiften och dess återuppringningsfunktion kan använda de flesta av de projektomfattande inställningarna. Detta är särskilt värdefullt när man arbetar med rumsliga lager i callback-funktionen.

Beroenden mellan uppgifter kan beskrivas med hjälp av funktionen addSubTask() i QgsTask. När ett beroende anges kommer uppgiftshanteraren automatiskt att avgöra hur dessa beroenden ska utföras. När det är möjligt kommer beroenden att utföras parallellt för att uppfylla dem så snabbt som möjligt. Om en uppgift som en annan uppgift är beroende av avbryts, kommer även den beroende uppgiften att avbrytas. Cirkulära beroenden kan leda till deadlocks, så var försiktig.

Om en uppgift är beroende av att ett lager är tillgängligt kan detta anges med funktionen setDependentLayers() i QgsTask. Om ett lager som en uppgift är beroende av inte är tillgängligt kommer uppgiften att avbrytas.

När uppgiften har skapats kan den schemaläggas för körning med hjälp av funktionen addTask() i uppgiftshanteraren. Genom att lägga till en uppgift i hanteraren överförs äganderätten till uppgiften automatiskt till hanteraren, och hanteraren rensar och tar bort uppgifter efter att de har körts. Schemaläggningen av uppgifterna påverkas av uppgiftsprioriteten, som ställs in i addTask().

Status för uppgifter kan övervakas med hjälp av signaler och funktioner i QgsTask och QgsTaskManager.

15.2. Exempel

15.2.1. Utökning av QgsTask

I det här exemplet utökar RandomIntegerSumTask QgsTask och kommer att generera 100 slumpmässiga heltal mellan 0 och 500 under en angiven tidsperiod. Om det slumpmässiga talet är 42 avbryts uppgiften och ett undantag tas upp. Flera instanser av RandomIntegerSumTask (med underuppgifter) genereras och läggs till i uppgiftshanteraren, vilket demonstrerar två typer av beroenden.

  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. Uppgift från funktion

Skapa en uppgift från en funktion (doSomething i det här exemplet). Funktionens första parameter kommer att innehålla QgsTask för funktionen. En viktig (namngiven) parameter är on_finished, som anger en funktion som ska anropas när uppgiften har slutförts. Funktionen doSomething i det här exemplet har ytterligare en namngiven parameter 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. Uppgift från en bearbetningsalgoritm

Skapa en uppgift som använder algoritmen qgis:randompointsinextent för att generera 50000 slumpmässiga punkter inom en angiven utsträckning. Resultatet läggs till i projektet på ett säkert sätt.

 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)

Se även: https://www.opengis.ch/2018/06/22/threads-in-pyqgis3/.