Библиотеки: Sequence

 

Sequence:

Последовательный запуск расчетов в параллельно-выполняющихся программах

Автор: fxsaber

 
Пример советника, который убьет большинство VPS.
#include <fxsaber\Sequence.mqh> // https://www.mql5.com/ru/code/31446

#define PRINT(A) Print(#A + " = " + (string)(A))

input datetime inFrom = D'2020.09.01'; // С какой даты анализировать историю

void OnInit()
{
  SEQUENCE Sequence; // Последовательный запуск расчетов
  
//  if (Sequence.Init()) // Раскомментируйте для последовательного выполнения.
  {
    MqlTick Ticks[];
    
    PRINT(CopyTicksRange(_Symbol, Ticks, COPY_TICKS_ALL, (long)inFrom * 1000));
    PRINT(TerminalInfoInteger(TERMINAL_MEMORY_USED));
    
    Sleep(10000); // Ждем освобождения CopyTicks-данных.
  }
}


Запускаем его на нескольких чартах разных символов. Для автоматизации сего действия использовал этот скрипт с inAmount = 5.


Результат.

2020.10.13 13:26:53.199 Test9 (AUDCAD,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 5406953
2020.10.13 13:26:53.326 Test9 (AUDCAD,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 2838
2020.10.13 13:26:53.528 Test9 (EURCHF,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 3430958
2020.10.13 13:26:53.807 Test9 (EURCHF,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 3144
2020.10.13 13:26:53.924 Test9 (EURUSD,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 4244747
2020.10.13 13:26:54.214 Test9 (EURUSD,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 3464
2020.10.13 13:26:54.344 Test9 (AUDCHF,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 4327679
2020.10.13 13:26:54.702 Test9 (AUDCHF,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 3797
2020.10.13 13:26:54.864 Test9 (GBPCHF,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 5340006
2020.10.13 13:26:55.457 Test9 (GBPCHF,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 4308
2020.10.13 13:26:55.666 Test9 (EURAUD,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 7730155
2020.10.13 13:26:55.756 Test9 (EURAUD,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 4316

Больше 4 Gb памяти потребовал Терминал для запуска этих шести советников. Заметьте, это нужно только для инициализации, но не для работы этих советников. Представьте, что запускаете Терминал с висящими советниками в нем. Если нет честных свободных 4 Gb RAM - почти катастрофа.


Теперь уберем комментарий этой строки в исходнике.

  if (Sequence.Init()) // Раскомментируйте для последовательного выполнения.

Тем самым включив последовательную инициализацию советников.


Смотрим на результат (после перекомпиляции).

2020.10.13 13:27:24.002 Test9 (AUDCAD,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 5406980
2020.10.13 13:27:24.021 Test9 (AUDCAD,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 1234
2020.10.13 13:27:35.407 Test9 (EURUSD,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 4244772
2020.10.13 13:27:35.422 Test9 (EURUSD,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 1095
2020.10.13 13:27:46.886 Test9 (GBPCHF,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 5340072
2020.10.13 13:27:46.905 Test9 (GBPCHF,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 1224
2020.10.13 13:27:58.293 Test9 (AUDCHF,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 4327724
2020.10.13 13:27:58.310 Test9 (AUDCHF,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 1114
2020.10.13 13:28:09.683 Test9 (EURCHF,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 3430999
2020.10.13 13:28:09.696 Test9 (EURCHF,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 1015
2020.10.13 13:28:21.339 Test9 (EURAUD,H1)       CopyTicksRange(_Symbol,Ticks,COPY_TICKS_ALL,(long)inFrom*1000) = 7730313
2020.10.13 13:28:21.363 Test9 (EURAUD,H1)       TerminalInfoInteger(TERMINAL_MEMORY_USED) = 1519


На запуске советников удалось уменьшить потребление памяти Терминалом более, чем на 2.5 Gb. Вероятность VPS-катастрофы (и слабых домашних машин) значительно уменьшилась.


Вот так оба запуска выглядят в динамике.

Последовательный запуск растянул общую инициализации по времени, но сумел удержать Терминал от огромного потребления ОЗУ.


На верхнем и нижнем графиках хорошо виден процесс параллельной инициализации(левый высокий пик) и шести последовательных инициализаций (шесть средних пиков).


ЗЫ Во время экспериментов выявился неприятный нюанс со Sleep - см. исходник.

 
То, как сейчас написано, подойдет только для однократной сериализации (каждый "желающий" после однократной обработки "отваливается"). Если нужно её делать многократно, то могут возникнуть проблемы из-за очередности вызовов таймеров (один "желающий" может занять ресурс последовательно несколько раз, а другой - ни раза). ИМХО, не хватает какой-либо реализации "очереди" и/или "приоритетов" на основе срока давности.
 
Stanislav Korotky:
То, как сейчас написано, подойдет только для однократной сериализации (каждый "желающий" после однократной обработки "отваливается"). Если нужно её делать многократно, то могут возникнуть проблемы из-за очередности вызовов таймеров (один "желающий" может занять ресурс последовательно несколько раз, а другой - ни раза). ИМХО, не хватает какой-либо реализации "очереди" и/или "приоритетов" на основе срока давности.

Да, очереди нет. Кто успел - того и тапки. Не заморачивался, т.к. самому нужна только инициализация. А если речь идет о какой-нибудь автооптимизации, то все же логично, что автор сделает механизм, как в примере из описания - PrevCalcTime.


Похоже, идея очереди - зло. Т.к. можно уйти в бесконечное ожидание.

 
fxsaber:

Похоже, идея очереди - зло. Т.к. можно уйти в бесконечное ожидание.

Как и в любом случае - зависит от реализации. При правильной не должно быть такого.

Более критично то, что при текущей реализации и общем названии (без специализации, что это только для однократного инита) - проблемы обеспечены.

 
Stanislav Korotky:

Как и в любом случае - зависит от реализации. При правильной не должно быть такого.

Один советник с таймером сутки, другой - секунда. Разве возможна в такой ситуация очередь?

Stanislav Korotky:

Более критично то, что при текущей реализации и общем названии (без специализации, что это только для однократного инита) - проблемы обеспечены.

Для конструктивной критики лучше какой-нибудь исходник привести. Например, пример из описания.
 
fxsaber:

Один советник с таймером сутки, другой - секунда. Разве возможна в такой ситуация очередь?

Для конструктивной критики лучше какой-нибудь исходник привести. Например, пример из описания.

Когда необходимости в очереди нет, она прозрачно переваривает запросы, как будто её нет.

Чтобы исходник привести, его надо сперва написать, даже если на основе описания ;-). Прямо сейчас примера пока нет.

 

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

  if ((TimeLocal() - PrevCalcTime >= 1 ) && // Если пришло время делать расчеты
      Sequence.IsFree())                    // и вычислительные ресурсы свободны

Мы же не может гарантировать, что в каком-то клиенте не случится расчет длиннее периода запроса ресурса.

Более наглядный способ - добавить параметры:

input int timerMs = 100;
input int periodSec = 10;
input int calcMs = 1000;

Заменить на них константы в коде а-ля:

void OnTimer()
{
  static datetime PrevCalcTime = 0;
  static int Amount = 0; // Количество расчетов

  SEQUENCE Sequence; // Последовательный запуск расчетов

  if ((TimeLocal() - PrevCalcTime >= periodSec) && // Если пришло время делать расчеты
      Sequence.IsFree())                    // и вычислительные ресурсы свободны
  {
    PrevCalcTime = TimeLocal();

    Sleep(calcMs); // Тяжелые расчеты.
    Print((string)ChartID() + " - " + __FUNCTION__ + ": " + (string)Amount++);
  }
}

Потом нужно запустить 3 экземпляра с periodSec=2.

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

 
Stanislav Korotky:

Когда экспертов будет много и все эти временные задержки сложатся непредсказуемым образом, гарантировать захват ресурса в конкретном экземпляре будет невозможно.

Понимаю, о чем речь. Похоже, описание неоднозначное. Нужен был механизм определения наличия свободного ресурса с запретом использования его другим. С этим библа справляется на ура. А вот последовательность все же ложится на плечи автора советника. Универсального решения либо нет, либо оно очень громоздкое: когда на решение уходит больше времени, чем требуется всем частным применениям.

 

Совмещение с FileSelectDialog, чтобы при запуске Терминала не возникала каша из диалоговых окон.

#include <fxsaber\Sequence.mqh> // https://www.mql5.com/ru/code/31446

void OnInit()
{
  SEQUENCE Sequence; // Последовательный запуск расчетов
  
  if (Sequence.Init()) // Дожидаемся освобождения вычислительных ресурсов.
  {
    string FileNames[];

    ChartSetInteger(0, CHART_BRING_TO_TOP, 0, true);
    ChartGetInteger(0, CHART_WINDOW_HANDLE); // Сдвинули очередь.
    
    FileSelectDialog(MQLInfoString(MQL_PROGRAM_NAME) + " " + _Symbol + " " + EnumToString(_Period),
                     NULL, NULL, FSD_ALLOW_MULTISELECT | FSD_FILE_MUST_EXIST, FileNames, NULL);
    ArrayPrint(FileNames);
  }
}
 

Иногда требуется провести тяжелый расчеты сразу на нескольких запущенных советниках. Такой параллельный расчет способен убить торговлю не только на своем терминале, но и на параллельно работающих. Причина - критическая нагрузка CPU.


Чтобы этого не происходило, желательно делать подобные расчеты последовательно. Ниже схема, как это можно сделать.

// Пример последовательных тяжелых расчетов без прерывания остальных действий советника.

#include <fxsaber\Sequence.mqh> // https://www.mql5.com/ru/code/31446

// Тяжелый расчет.
void Calculate()
{
  Print((string)ChartID() + " - Run.");
  Sleep(1000);
  Print((string)ChartID() + " - Done.");
}

void OnChartEvent( const int id, const long& lparam, const double& dparam, const string& sparam )
{
  switch (id)
  {
    case CHARTEVENT_KEYDOWN: // Если нажали на клавишу - отдаем приказы на расчет.
      Print(lparam);
      Print("Calculate All!");
      
      for (long Chart = ::ChartFirst(); Chart != -1; Chart = ::ChartNext(Chart))      
        EventChartCustom(Chart, 0, 0, 0, NULL);
      
      break;
    
    case CHARTEVENT_CUSTOM: // Если пришел приказ на расчет.
    {
      SEQUENCE Sequence; // Последовательный запуск расчетов
      
      if (Sequence.IsFree()) // Вычислительные ресурсы свободны
        Calculate();         // Считаем
      else
      {
        Sleep(0); // https://www.mql5.com/ru/forum/170952/page207#comment_23627280
        
        EventChartCustom(0, (ushort)(id - CHARTEVENT_CUSTOM), lparam, dparam, sparam); // Повторяем приказ себе же на расчет.
      }
    }    
  }
}


Результат параллельного запуска пяти таких советников.

2021.07.25 12:34:48.823 Calculate All!
2021.07.25 12:34:48.825 132503570123939383 - Run.
2021.07.25 12:34:49.844 132503570123939383 - Done.
2021.07.25 12:34:49.844 132503570123939385 - Run.
2021.07.25 12:34:50.857 132503570123939385 - Done.
2021.07.25 12:34:50.858 132503570123939382 - Run.
2021.07.25 12:34:51.872 132503570123939382 - Done.
2021.07.25 12:34:51.872 132503570123939384 - Run.
2021.07.25 12:34:52.891 132503570123939384 - Done.
2021.07.25 12:34:52.892 132503570123939381 - Run.
2021.07.25 12:34:53.907 132503570123939381 - Done.


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