Выполнение асинхронных запросов

<< Click to Display Table of Contents >>

Navigation:  Проект > Элементы дерева объектов > Палитра ФБ > Служебные > Скрипт > Руководство и примеры > Работа с архивом данных >

Выполнение асинхронных запросов

В прошлом разделе мы выполнили обработку архива - загрузили набор значений за определенный интервал времени, а затем по очереди прошли по его элементам. В нашем примере архив был небольшим, и выборка тоже была короткая, но что будет если мы попробуем загрузить архив за несколько месяцев, и не по одной переменным, а например по 10? Естественно такой запрос может оказаться достаточно длительный. А что будет, если будет выполнятся обращение к архиву находящегося на удаленной базе данных, с которой пропала связь? Запрос будет еще дольше - и завершится только по таймауту соединения. При этом, пока выполняется запрос к архиву, весь поток выполнения будет остановлен. Данная проблема касается не только работы с архивом, но и других длительных операций - SQL запросов, обращений к WEB или FTP серверам, открытие больших файлов и т.д.

Здесь нужно отступить от темы, и рассказать про потоки в MasterSCADA. В MasterSCADA есть объекты первого уровня (т.е те что находятся одним уровнем ниже корневого):

sluzhebnie_skript_rukovodstvo_i_primery_rabota_s_arhivom_dannyh_vypolnenie_asinhronnyh_zaprosov

Каждый из таких объектов выполняется в отдельном потоке. Объекты вложенные в объекты первого уровня - выполняются в его потоке. Т.е. в примере на скриншоте будет 5 потоков. До версии 3.12 на уровне системы MasterSCADA была настройка Количество рабочих потоков, со значением 3 по умолчанию - то есть количество потоков ограничивалось тремя. В настоящий момент по умолчанию количество потоков не ограничивается - за это отвечает настройка Выполнение в отдельных потоках. Убедитесь что в вашем проекте данный флаг установлен.

sluzhebnie_skript_rukovodstvo_i_primery_rabota_s_arhivom_dannyh_vypolnenie_asinhronnyh_zaprosov1

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

Если, как, например, в прошлом примере, вы не делаете больших выборок - значения ограничены архивом за 1-2 дня, и делаются по запросу оператора, то, как правило, можно не беспокоится о данной проблеме - даже если после нажатия кнопки оператором, часть объектов не будут обновляться 1-2 секунды, то ничего страшного не произойдет.

Если все же необходимо, чтобы обработка архивов не мешала работе, то можно поступить двумя способами. Первый способ, очень простой - нужно положить Скрипт (и возможно какие-то элементы для его управления) в отдельный объект, и у этого объекта поставить флаг Собственный цикл.

sluzhebnie_skript_rukovodstvo_i_primery_rabota_s_arhivom_dannyh_vypolnenie_asinhronnyh_zaprosov2

Установка данного флага выводит скрипт в независимый поток, и теперь его работа не будет влиять на опрос остального объекта в который он добавлен. Если таких скриптов у вас не много - это самое простое решение.

Если же скриптов работающих с архивом у вас много, и вы не можете сгруппировать их в одном объекте, то в таком случае можно несколько изменить код скрипта - сделать его запрос асинхронным.

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

Рассмотрим в качестве примера скрипт, который будет долго выполняться - в цикле мы будем считать до тысячи, и в каждом шаге цикла будем делать Sleep на 1 миллисекунду и посмотрим что будет происходить с переменными проекта.

sluzhebnie_skript_rukovodstvo_i_primery_rabota_s_arhivom_dannyh_vypolnenie_asinhronnyh_zaprosov3

Сам код и структура скрипта очевидны и комментариях не нуждаются. Переменная Команда 1 - имитационная, с режимом имитации Шум. По ней мы будем отслеживать что наш поток "висит".

Запустим режим исполнения и подадим сигнал на вход Начать.

sluzhebnie_skript_rukovodstvo_i_primery_rabota_s_arhivom_dannyh_vypolnenie_asinhronnyh_zaprosov4

На время работы скрипта, состояние входа Начать остается Выкл (дерево еще не обновилось, потому что цикл не завершен), а Команда 1 не меняется. Спустя некоторое время значения появятся:

sluzhebnie_skript_rukovodstvo_i_primery_rabota_s_arhivom_dannyh_vypolnenie_asinhronnyh_zaprosov5

Попробуем избавится от этой проблемы. Сначала попробуем простой вариант решения - поставим флаг Собственный цикл.

sluzhebnie_skript_rukovodstvo_i_primery_rabota_s_arhivom_dannyh_vypolnenie_asinhronnyh_zaprosov6

Запустим режим исполнения и проверим - теперь Команда 1 будет меняться, пока вычисляется скрипт.

Теперь попробуем изменить код нашего скрипта. Сначала сбросим флаг Собственный цикл.

sluzhebnie_skript_rukovodstvo_i_primery_rabota_s_arhivom_dannyh_vypolnenie_asinhronnyh_zaprosov7

В секцию using необходимо добавить два пространства имен:

using System.Threading.Tasks;
using System.Threading;

Приведем код целиком:

bool? M = false;
public override async void Execute()
{
    if (Начать == true && M == false && ИдетВычисление != true)
    {
        ИдетВычисление = true;
        await Calculate(); //асинхронный вызов метода Calculate
        ИдетВычисление = false;
    }
    M = Начать;
}
private Task Calculate()
{
    return Task.Run(() =>
    {
        uint Val = 0;
        for (uint i = 0; i < 1000; i++)
        {
            Val = Val + i;
            System.Threading.Thread.Sleep(1);
        }
        HostFB.FireEvent(1, "Вычисления готовы");
        Результат = Val;
    });
}

Обратите внимание что метод Execute помечен ключевым словом async - это означает что данный метод может вызывать асинхронные запросы.

Метод Calculate имеет возвращаемый параметр - Task, т.е. задачу. Это означает что данный метод можно вызывать асинхронно. Далее следует код в котором создается задача, и происходит ее запуск методом Run. Внутри данного лямбда-выражения и находится наш имитационный код. В конце вычисленное значение записывается на выход скрипта Результат.

Метод Calculate вызывается с помощью оператора await. В момент вызова метод вызывается, запускается задача, код скрипта останавливается, но вызвавший метод Execute поток отпускается и продолжает свою работу. Вызову метода предшествует запись значения true на выход ИдетВычисление - это сигнал, что идет обработка. При этом, в условии проверяется что идет вычисление не равен true - чтобы не запустить новое вычисление пока не закончилось старое. После того как асинхронный метод закончил работу, ИдетВычисление сбрасывается в false.

Как можно выполнить передачу и получение значения из асинхронной операции? Для пример рассмотрим ту же самую задачу, но теперь сделаем, чтобы вычисленное значение возвращалось из скрипта и записывалось на выход в методе Execute, и в нем же формировались сообщения. В качестве входного аргумента мы будем передавать до какого значения вести счет.

Исправленный код ниже:

bool? M = false;
public override async void Execute()
{
    if (Начать == true && M == false && ИдетВычисление != true)
    {
        ИдетВычисление = true;
        var Res = await Calculate(1000);   //асинхронный вызов метода Calculate с параметром 1000
        HostFB.FireEvent(1, "Вычисления готовы");
        Результат = Res;
        ИдетВычисление = false;
    }
    M = Начать;
}
private Task<uint> Calculate(uint Count)
{
    return Task<uint>.Run(() =>
    {
        uint Val = 0;
        for (uint i = 0; i < Count; i++)
        {
            Val = Val + i;
            System.Threading.Thread.Sleep(1);
        }
        return Val;
    });
}

С точки зрения передачи значений в функцию никаких трудностей нет - просто передается входное значение, как и в случае простого вызова метода.

С возвратом все несколько сложнее. При объявлении метода необходимо указать не просто Task, а также тип возвращаемого значения в угловых скобках, в нашем случае это тип uint. Аналогично в теле метода, необходимо создать и вернуть Task с таким же типом данных, а затем, уже в лямбда-функции вернуть значение данного типа.

Затем уже в методе Execute, мы проверяем есть ли значение в переменной Val, и в зависимости от этого выдаем нужное сообщение, а результат пишем на выход.

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

Какой из вариантов записи значений на выходы использовать - писать в асинхронном методе или же вернуть значение и записать в Execute? В первом случае есть риск того, что запись на выходы попадет между циклами опроса - например, вы записываете значение в 10 выходов, выполнили необходимую обработку в асинхронном запросе и в нем же пишите значения на теги, 5 тегов вы записали и они записались в одном цикле опроса объекта, а оставшиеся 5 - уже в следующем. Если данные выходы используются для вывода на мнемосхему (чаще всего так и будет) - это не страшно, но если же эти значения потом используются в каком-то алгоритме, то тогда следует вернуть эти значения, и записать в методе Execute - тогда они запишутся одновременно.

Готовый пример с двумя вариантами скрипта можно скачать по ссылке.

Примечание. Ключевые слова async и await доступны начиная с версии 3.13. Если у вас более старая версия, то данный код не скомпилируется. Вам нужно или обновиться до текущей версии, или создать асинхронную операцию с помощью метода Task.Start.

Теперь попробуем адаптировать наш пример с прошлого шага под асинхронный запрос:

public partial class ФБ : ScriptBase
{
    struct ValStruct
    {
        public double? Val;
        public DateTime? Tim;
        public ValStruct(double? ValIn, DateTime? TimIn)
        {
            Val = ValIn;
            Tim = TimIn;
        }
    }
    bool? M = false;
    public override async void Execute()
    {
        if (Найти == true && M == false && Начало.HasValue && Конец.HasValue && Начало < Конец)
        {
            ValStruct St = await GetMax();
            Значение = St.Val;
            МеткаВремени = St.Tim;
        }
        M = Найти;
    }
    private Task<ValStruct> GetMax()
    {
        return Task<ValStruct>.Run(() =>
        {
            var elem = HostFB.InputGroup.GetPin("Вход").TreePinHlp;
            var k = elem.DataArchiveItem;
            DateTime EndTime = Конец.Value.ToUniversalTime();
            DateTime StartTime = Начало.Value.ToUniversalTime();
            var mas = k.Read(StartTime, EndTime, false);
            double? Val = null;
            DateTime? TimeStamp = null;
            foreach (var element in mas)
            {
                if (Val.HasValue == false || Convert.ToDouble(element.Value) > Val.Value)
                {
                    Val = Convert.ToDouble(element.Value);
                    TimeStamp = element.Time.ToLocalTime();
                }
            }
            return new ValStruct(Val, TimeStamp);
        });
    }
}

По сути весь код перенесен в асинхронный метод, из которого возвращается структура из значения double и DateTime, которая потом записывается на выходы.

Пример можно скачать по ссылке.

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