Os trechos de código desta página precisam das seguintes importações se você estiver fora do console do pyqgis:
1 2 3 4 5 6 7 8 9 10 | from qgis.core import (
QgsProcessingContext,
QgsTaskManager,
QgsTask,
QgsProcessingAlgRunnerTask,
Qgis,
QgsProcessingFeedback,
QgsApplication,
QgsMessageLog,
)
|
15. Tarefas - trabalho pesado em segundo plano¶
15.1. Introdução¶
O processamento em segundo plano usando threads é uma maneira de manter uma interface de usuário responsdendo quando o processamento pesado está em andamento. As tarefas podem ser usadas para obter threading no QGIS.
Uma tarefa (QgsTask
) é um contêiner para o código a ser executado em segundo plano, e o gerenciador de tarefas (QgsTaskManager
) é usado para controlar a execução das tarefas. Essas classes simplificam o processamento em segundo plano no QGIS, fornecendo mecanismos para sinalização, relatórios de progresso e acesso ao status dos processos em segundo plano. As tarefas podem ser agrupadas usando subtarefas.
O gerenciador de tarefas global (encontrado com QgsApplication.taskManager()
) é normalmente usado. Isso significa que suas tarefas podem não ser as únicas controladas pelo gerenciador de tarefas.
Existem várias maneiras de criar uma tarefa QGIS:
Crie sua própria tarefa estendendo
QgsTask
class SpecialisedTask(QgsTask): pass
Criar uma tarefa a partir de uma função
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)
Criar uma tarefa a partir de um algoritmo de processamento
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)
Aviso
Any background task (regardless of how it is created) must NEVER use any QObject that lives on the main thread, such as accessing QgsVectorLayer, QgsProject or perform any GUI based operations like creating new widgets or interacting with existing widgets. Qt widgets must only be accessed or modified from the main thread. Data that is used in a task must be copied before the task is started. Attempting to use them from background threads will result in crashes.
Dependências entre tarefas podem ser descritas usando a função addSubTask
de QgsTask
. Quando uma dependência é declarada, o gerenciador de tarefas determina automaticamente como essas dependências serão executadas. Sempre que possível, as dependências serão executadas em paralelo para satisfazê-las o mais rápido possível. Se uma tarefa da qual outra tarefa depende for cancelada, a tarefa dependente também será cancelada. Dependências circulares podem tornar possíveis impasses, portanto, tenha cuidado.
Se uma tarefa depende da disponibilidade de uma camada, isso pode ser indicado usando a função setDependentLayers
de QgsTask
. Se uma camada da qual uma tarefa depende não estiver disponível, a tarefa será cancelada.
Após a criação da tarefa, ela pode ser agendada para execução usando a função addTask
do gerenciador de tarefas. A adição de uma tarefa ao gerente transfere automaticamente a propriedade dessa tarefa para o gerente, e o gerente limpa e exclui as tarefas após a execução. O agendamento das tarefas é influenciado pela prioridade da tarefa, que é definida em addTask
.
O status das tarefas pode ser monitorado usando QgsTask
e sinais e funções de QgsTaskManager
.
15.2. Exemplos¶
15.2.1. Estendendo QgsTask¶
Neste exemplo, RandomIntegerSumTask
estende QgsTask
e gerará 100 números inteiros aleatórios entre 0 e 500 durante um período especificado. Se o número aleatório for 42, a tarefa será abortada e uma exceção será gerada. Várias instâncias de RandomIntegerSumTask
(com subtarefas) são geradas e adicionadas ao gerenciador de tarefas, demonstrando dois tipos de dependências.
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. Tarefa da função¶
Crie uma tarefa a partir de uma função (doSomething
neste exemplo). O primeiro parâmetro da função conterá QgsTask
para a função. Um parâmetro importante (nomeado) é on_finished
, que especifica uma função que será chamada quando a tarefa for concluída. A função doSomething
neste exemplo possui um parâmetro adicional com nome 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. Tarefa de um algoritmo de processamento¶
Crie uma tarefa que use o algoritmo qgis:randompointsinextent para gerar 50000 pontos aleatórios dentro de uma extensão especificada. O resultado é adicionado ao projeto de forma 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)
|
Veja também: https://opengis.ch/2018/06/22/threads-in-pyqgis3/.