# Работа с длительными операциями

## Задачи

Скрипты на python выполняются в основном потоке приложения или по другому в потоке интeрфейса. Если операция будет выполняться слишком долго, то интерфейс «зависнет» и будет невозможно со стороны пользователя понять это ошибка или программа все-таки продолжает выполнять свою работу. Чтобы этого избежать длительные вычисления следует выносить в фоновые потоки. В потоке интерфейса во время выполнения фоновой задачи есть возможность показывать прогресс операции.

Чтобы превратить пользовательскую функцию в задачу, нужно использовать класс `axipy.Task`.

Пример использования.

    import time
    import axipy

    def user_heavy_function(t: axipy.Task, arg1: int, arg2: str):
        print(f"Переданные аргументы: {arg1}, {arg2}.")
        time.sleep(2)
        return 1

    task = axipy.Task(user_heavy_function)
    result = task.run_and_get("Hello", "world")
    print(result)

    """
    >>> Переданные аргументы: Hello, world.
    >>> 1
    """

При использовании метода `axipy.Task.start()`, для получения результата, нужно подписаться на сигнал `axipy.Task.finished`.

Пример использования.

    import time
    import axipy

    def user_heavy_function(t: axipy.Task, arg1: int, arg2: str):
        print(f"Переданные аргументы: {arg1}, {arg2}.")
        time.sleep(2)
        return 1

    task = axipy.Task(user_heavy_function)

    def finished(t: axipy.Task):
        print(t.status)
        print(t.result)

    task.finished.connect(finished)
    task.start("Hello", "world")

    """
    >>> Переданные аргументы: Hello, world.
    >>> Status.SUCCESS
    >>> 1
    """

Чтобы сделать поддержку типизированных аргументов и возвращаемого значения для задачи, рекомендуется наследование от базового класса `axipy.Task`.

Пример использования.

    import axipy

    class SummTask(axipy.Task):

        def __init__(self, *args, **kwargs) -> None:
            super().__init__(self.run, *args, **kwargs)

        def run(self, arg1: float, arg2: float) -> float:
            self.value += 1
            return arg1 + arg2

        def run_and_get(self, arg1: float, arg2: float) -> float:
            return super().run_and_get(arg1, arg2)

    result = SummTask().run_and_get(1.0, 2.3)
    print(result)

    """
    >>> 3.3
    """

## Представление прогресса операции

Класс `axipy.DialogTask` дополняет класс `axipy.Task` наличием диалога для отображения прогресса операции. Для обмена информацией между выполняемой задачей и элементом, отображающим прогресс, используется ссылка на экземпляр задачи (`axipy.Task`), передаваемая первым аргументом в пользовательскую функцию. Типичный вариант использования выглядит следующим образом:

    import axipy
    import time

    def user_heavy_function(dt: axipy.DialogTask, loop_range: int):
        dt.range = dt.Range(0, loop_range)
        dt.message = 'Прогресс выполнения'
        for i in range(loop_range):
            time.sleep(0.5)
            dt.value += 1
            if dt.is_canceled:
                return False
        return True

    dialog_task = axipy.DialogTask(user_heavy_function)
    dialog_task.title = 'Заголовок'
    result = dialog_task.run_and_get(10)
    print(result)
    """
    >>> Result: True
    """

Вместо `axipy.Task.is_canceled` можно использовать `axipy.Task.raise_if_canceled` если нужно выйти из нескольких вложенных вызовов функций или циклов.

## Создание пользовательского виджета для отображения прогресса

    import axipy
    import time
    import PySide2.QtWidgets
    import PySide2.QtCore

    task_max_range = 20

    def heavy_function(t: axipy.Task):
        for i in range(task_max_range):
            time.sleep(0.5)
            t.value += 1
            t.raise_if_canceled()
        return 1

    class CustomDialog(PySide2.QtWidgets.QDialog):

        def __init__(self, *args, **kwargs) -> None:
            super().__init__(*args, **kwargs)

            vbox = PySide2.QtWidgets.QVBoxLayout()
            self.pbar = PySide2.QtWidgets.QProgressBar()
            self.pbar.setRange(0, task_max_range)
            vbox.addWidget(self.pbar)

            bbox = PySide2.QtWidgets.QDialogButtonBox()
            bbox.setStandardButtons(
                PySide2.QtWidgets.QDialogButtonBox.Cancel
            )
            bbox.rejected.connect(self.bbox_reject)
            vbox.addWidget(bbox)

            self.setLayout(vbox)

        @PySide2.QtCore.Slot()
        def bbox_reject(self) -> None:

            def ask() -> PySide2.QtWidgets.QMessageBox.StandardButton:
                return PySide2.QtWidgets.QMessageBox.question(
                    axipy.view_manager.global_parent,
                    "Отмена",
                    "Отменить задачу?",
                )

            answer = ask()
            if answer == PySide2.QtWidgets.QMessageBox.Yes:
                self.reject()

        @PySide2.QtCore.Slot()
        def add_progress(self) -> None:
            self.pbar.setValue(self.pbar.value() + 1)

    dialog = CustomDialog(axipy.view_manager.global_parent)
    task = axipy.Task(heavy_function)
    task.value_changed.connect(dialog.add_progress)
    task.finished.connect(dialog.close)
    dialog.rejected.connect(task.cancel)
    task.start()
    if task.status not in (
            task.Status.SUCCESS,
            task.Status.CANCELED,
            task.Status.ERROR,
    ):
        dialog.exec_()

## Выполнение задач и многопоточность

Важно понимать, что задачи будут выполняться не в потоке интерфейса, поэтому внутри этих задач нельзя отображать никакие графические элементы (`PySide2.QtWidgets.QWidget`). Так же нужно внимательно следить за тем какие ресурсы могут использоваться несколькими потоками и при необходимости использовать различные механизмы синхронизации ( мьютексы, локи и т.д.). Общее правило при работе с несколькими потоками следующее: старайтесь чтобы каждая задача содержала в себе все необходимые данные для выполнения. Синхронизацию, если она необходима, следует использовать только в момент получения результата. Это упростит код и сведет к минимум количество ошибок.

Переданные на выполнения задачи ставятся в общую очередь, которая распределяется между физическими ядрами процессора в порядке добавления. Поэтому нет гарантии, что переданная задача выполнится мгновенно, а так же то что группа задач будет выполняться именно в порядке добавления. Если задача предполагает долгое ожидание без интенсивных нагрузок на процессор, то лучше воспользоваться стандартными python потоками. Типичные примеры таких задач это загрузка ресурсов с диска или скачивание файлов по сети.