重要

翻訳は あなたが参加できる コミュニティの取り組みです。このページは現在 92.31% 翻訳されています。

15. タスク - バックグラウンドで重い仕事をする

ヒント

pyqgisコンソールを使わない場合、このページにあるコードスニペットは次のインポートが必要です:

 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. はじめに

スレッドを使用したバックグラウンド処理は、重い処理が行われているときに応答性の高いユーザーインターフェイスを維持するための方法です。タスクはQGISでスレッドを実行するために使用できます。

タスク( QgsTask )はバックグラウンドで実行されるコードのコンテナです。そしてタスクマネージャ( QgsTaskManager )は タスクの実行を制御するために使用されます。 これらのクラスは、シグナリング、進捗報告、およびバックグラウンドプロセスのステータスへアクセスするためのメカニズムを提供することによって、QGISのバックグラウンド処理を単純化します。 タスクはサブタスクを使用してグループ化できます。

通常、グローバルタスクマネージャ(QgsApplication.taskManager() で見つかります)が使用されます。つまり、タスクマネージャによって制御されるタスクはあなたのタスクだけとは限りません。

QGISタスクを作成する方法はいくつかあります:

  • QgsTask を拡張することで自分のタスクを作成する

    class SpecialisedTask(QgsTask):
        pass
    
  • 関数からタスクを作成

     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)
    
  • プロセシングアルゴリズムからタスクを作成する

    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)
    

警告

どのバックグラウンドタスクも(どのように作成されたかに関わらず)、QgsVectorLayerやQgsProjectへのアクセスや、新しいウィジェットの作成や既存のウィジェットとのインタラクションのようなGUIベースの操作のように、メインスレッドに存在するQObjectを決して使ってはいけません。Qtウィジェットへのアクセスや変更はメインスレッドからのみ行ってください。タスクで使用されるデータは、タスクが開始される前にコピーされなければなりません。バックグラウンドのスレッドからそれらを使用しようとするとクラッシュします。

Moreover always make sure that context and feedback live for at least as long as the tasks that use them. QGIS will crash if, upon completion of a task, QgsTaskManager fails to access the context and feedback against which the task was scheduled.

注釈

It is a common pattern to call setProject() shortly after calling QgsProcessingContext. This allows the task as well as its callback function to use most of the project-wide settings. This is especially valuable when working with spatial layers in the callback function.

タスク間の依存関係は QgsTaskaddSubTask() 関数を使って記述することができます。依存関係が指定されると、タスクマネージャはこれらの依存関係がどのように実行されるかを自動的に決定します。可能な限り、依存関係はできるだけ早く満たすために並列に実行されます。他のタスクが依存するタスクがキャンセルされると、依存するタスクもキャンセルされます。循環依存関係はデッドロックを引き起こす可能性があるので、注意が必要です。

タスクが利用可能なレイヤに依存している場合、 QgsTasksetDependentLayers() 関数を用いてそのことを指定することができます。タスクが依存するレイヤが利用できない場合、そのタスクはキャンセルされます。

タスクが作成されると、タスクマネージャの addTask() 関数を使用して実行をスケジュールすることができます。タスクをマネージャに追加すると、そのタスクの所有権は自動的にマネージャに移り、マネージャは実行後のタスクをクリーンアップして削除します。タスクのスケジューリングはタスクの優先度に影響されます。この優先度は addTask() で設定します。

タスクの状態は QgsTask および QgsTaskManager のシグナルと関数を使って監視できます。

15.2.

15.2.1. QgsTaskを拡張する

この例では RandomIntegerSumTaskQgsTask を拡張し、指定された期間中に0から500の間の100個のランダムな整数を生成します。 乱数が42の場合、タスクは中止され、例外が発生します。 (サブタスク付きの) RandomIntegerSumTask のいくつかのインスタンスが生成されてタスクマネージャに追加され、2種類の依存関係を実証します。

  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. 関数からのタスク

関数からタスクを作成します(この例では doSomething )。 関数の最初のパラメータは関数の QgsTask を持ちます。 重要な(名前付き)パラメータは on_finished です。これはタスクが完了したときに呼ばれる関数を指定します。 この例の doSomething 関数は追加の名前付きパラメータ 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. プロセッシングアルゴリズムからのタスク

qgis:randompointsinextent アルゴリズムを使って、指定された範囲内に50000個のランダムな点を生成するタスクを作成します。 結果は安全な方法でプロジェクトに追加されます。

 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)

次も参照すること:https://www.opengis.ch/2018/06/22/threads-in-pyqgis3/