Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
07.04.2026 15:50:21
Цитата
nikolz написал: Алиса отвечает: Не удалось найти информацию о связи между уменьшением непроизвольных движений и снижением риска падений
Плохо ищет: Обзор от ИИ Уменьшение непроизвольных движений (дерганий) напрямую связано со снижением частоты падений, улучшением координации и стабилизацией нервной системы.
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
07.04.2026 13:17:08
Цитата
VPM написал: У меня ломается система, пытаюсь установить причины и довести событийную модель "до ума".
Это результат применения ваших "подходов". Цитирую себя: "Меньше дергаешься, реже падаешь".
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Это массовая подача заявок с ограничением < 50 заявок в секунду? И до этого вы бы не додумались, если бы вы не прочитали про HFT?
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
07.04.2026 11:09:57
Цитата
VPM написал: Ряд торговых задач решается только с HFT подходом даже в рамках QUIK
Из интернета: "Высокочастотный трейдинг (HFT — High-Frequency Trading) — это вид алгоритмической торговли, использующий мощные компьютеры и сверхскоростные алгоритмы для совершения тысяч операций за доли секунды." Вы считаете HFT торговлей в QUIK подачу заявок с ограничением < 50 заявок в секунду и с получением данных с биржи с задержкой > 1 секунды?
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Вы историк или торговец ? Для принятия решений в роботе вам недостаточно текущего состояния вашего счета (которое отображается в его таблицах), а также текущих котировок и их истории (в ds)?
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
07.04.2026 10:00:36
Цитата
nikolz написал: не использую таблицу заявок терминала ВООБЩЕ.
То есть, у вас, до сих пор, в роботе не реализовано нормальное восстановление продолжения его работы при его перезапуске по любой причине? Например, ваш робот подал заявку и тут же упал (почему бы нет?). Вы его смогли починить через 1 час и запустили. И он, не используя таблицу заявок терминала ВООБЩЕ, ждет появления OnOrder? ----------
nikolz написал: Особенно если у вас HFT который может выставлять и снимать сотни заявок
Только сумасшедший решиться использовать QUIK для HFT.
Баг SearchItems с datetime.mcs
Пользователь
Сообщений: Регистрация: 12.05.2020
06.04.2026 16:53:41
Цитата
TGB написал: funduk нашел баг в часто используемой функции SearchItems. Я это подтвердил (QUIK 12.8.3.3). Почему элементарная ошибка в часто используемой функции не исправляется? Чего не хватает поддержке для этого?
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
06.04.2026 16:40:53
Цитата
VPM написал: Правильное решение нашлось. НЕ искать order_num через trade_num. Вместо этого, использовать OnOrder как источник истины.Производственный конвейер (Production pipeline) 1. В OnTransReply trans_id > сохраняем (pending); 2. В OnOrder order_num находим ордер. Следовательно, правильная модель связывания следующая Strategy > OrderExecutor > QUIK: QUIK: trans_id > order_num > trade_num OnTrade: trade > order_num > НАШ ORDER Связка через order_num — это не просто улучшение, это - > единственный надёжный способ устранить position drift в QUIK, зафиксирую это как архитектурный стандарт!
Ну и зачем так сложно? Вы хотите завести пользователей в болото ? Когда это можно реализовать проверкой (с использованием trans_id) в таблицах order, stop_order. И эту проверку надо выполнять только, когда изменяется количество записей в них. Единственным коллбеком, который имеет смысл, при этом, обрабатывать, фильтруя по созданным trans_id, это OnTransReply, так как в нем может быть информация о причине отказа выставления заявки.
Баг SearchItems с datetime.mcs
Пользователь
Сообщений: Регистрация: 12.05.2020
03.04.2026 07:20:31
funduk нашел баг в часто используемой функции SearchItems. Я это подтвердил (QUIK 12.8.3.3). Почему элементарная ошибка в часто используемой функции не исправляется? Чего не хватает поддержке для этого?
Гарантируется ли вызов колбэка при получении Квиком новых данных?, Вопросы разработчикам QUIK
Пользователь
Сообщений: Регистрация: 12.05.2020
22.03.2026 13:49:33
Цитата
Йцукен написал: И при подключении к серверу тот отдаёт клиенту все пропущенные данные по заявкам/сделкам за текущую торговую сессию.
Мы все таки обсуждали работу клиента в текущей сессии. Восстановление взаимодействия клиента QUIK с сервером это отдельная песня. И что то я писал об этом в соей ветке.
Гарантируется ли вызов колбэка при получении Квиком новых данных?, Вопросы разработчикам QUIK
Пользователь
Сообщений: Регистрация: 12.05.2020
22.03.2026 12:50:31
Цитата
Йцукен написал: Да нет никакого буфера на стороне сервера. Все заявки и сделки на сервере хранятся в БД SQL.
Вы думаете, что буфере реализуются только в оперативной памяти?
Цитата
Йцукен написал: И при подключении к серверу тот отдаёт клиенту все пропущенные данные по заявкам/сделкам за текущую торговую сессию. Возможно, даже не пропущенные, а все, в т.ч. и полученные раннее, а клиент на своей стороне уже смотрит, какие данные он показывал, а каких ещё не было, и вызывает соответствующий колбэк.
Оказывается вы все знали до ваших экспериментов . Вы разработчик сервера QUIK?
Гарантируется ли вызов колбэка при получении Квиком новых данных?, Вопросы разработчикам QUIK
Пользователь
Сообщений: Регистрация: 12.05.2020
22.03.2026 11:51:50
Цитата
Йцукен написал: Так чё ж они не доступны после выхода из колбэка OnParam?
Поток, который мог бы их показать занят тем что генерит:
Но вообще, я написал что должен быть буфер, и если, вдруг, он реализован на стороне сервера, то это архитектурный дефект. Я же написал:
Цитата
TGB написал: В клиент-серверных приложениях нормальные разработчики стараются реализовывать буфера в клиенте.
Гарантируется ли вызов колбэка при получении Квиком новых данных?, Вопросы разработчикам QUIK
Пользователь
Сообщений: Регистрация: 12.05.2020
22.03.2026 10:07:11
Цитата
Йцукен написал: если отключить интернет через несколько секунд после отправки транзакций, когда ответ по транзакциям уже должен прийти, то никаких новых данных в main не будет. Т.е., версия с буфером не подтверждается.
А если предположить, что пока вы отключали интерне, данные уже оказались в буфере? Как вы думаете где могут "шляться" данные созданные 12:03:25.000 а выданные в коллбеке в 12:06:00.86? В клиент-серверных приложениях нормальные разработчики стараются реализовывать буфера в клиенте.
Гарантируется ли вызов колбэка при получении Квиком новых данных?, Вопросы разработчикам QUIK
Пользователь
Сообщений: Регистрация: 12.05.2020
21.03.2026 10:29:07
Цитата
Nikolay написал: Не только очередь, а еще ack флаги. Сама очередь ничего не гарантирует.
В данном случае я не пишу как должна быть устроена очередь (об этом я пишу в другой ветке). Я написал, что очередь требуется чтобы не терять события коллбеков при v1 <= v2.
Гарантируется ли вызов колбэка при получении Квиком новых данных?, Вопросы разработчикам QUIK
Пользователь
Сообщений: Регистрация: 12.05.2020
21.03.2026 10:14:00
Цитата
Йцукен написал: А я вижу, что новые данные в main доступны только после выхода из OnParam. Когда эти данные пришли - не вижу.
Да в main их не видно, но я имею ввиду QUIK. Если вы сделаете задержку в OnParam 10000, то это не значит, что биржа будет ждать перед отсылкой данных о выставлении заявки 10 сек. пока один из 100000 терминалов QUIK не разблокирует свой поток обработки коллбеков.
Гарантируется ли вызов колбэка при получении Квиком новых данных?, Вопросы разработчикам QUIK
В выложенном коде отсутствует таблица transaction (шаблон заявки). Результат вашего теста показывает, что все коллбеки обрабатываются в одном потоке и когда он блокируется (в OnParam на 3000 млс.), все коллбеки перестают обрабатываться, но данные в QUIK при этом приходят (это видно в потоке main). Понятно, что для того, чтобы события коллбеков при этом не терялись, в QUIK должна быть внутренняя служебная(ые) очередь. Эта очередь может гарантировать отсутствие потерь событий только при
В меню запуска скриптов есть колонка "Память. KB". При выполнении скрипта у меня размер памяти пилообразно меняется. Период этой пилы и есть интервал сбора мусора. Продолжительность сбора мусора я определил, в своем отладочном меню скрипта, несколько раз выполнив, при приближении размера памяти к максимуму, код: collectgarbage('restart').
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
14.03.2026 08:31:26
Цитата
Oleg Kuzembaev написал: Ваше пожелание зарегистрировано. Мы постараемся рассмотреть его и сообщить Вам результаты анализа. Впоследствии, по результатам анализа, будет приниматься решение о реализации пожелания в будущих версиях ПО.
Здравствуйте. Спасибо за ответ. ---------------- Вариант реализации очередей с добавлениями. 1. //#define win32_API // В реализации очередей используется win32_API 2. const int QUEUE_RECORDING_MODE = 0; // Режим записи в очереди: 0 - без сигналов (более эффективный чем с сигналами); 1 - с сигналами. Режим записи в очереди без сигналов добавлен т.к. выдача сигнала "тяжелая" системная функция, в ~100 раз по времени ЦП затратнее, чем одна запись в очередь. В этом режиме сигналы пишущим потоком не выдаются. В потоках чтения реализованы два цикла: 1) первый внешний цикл: пользовательский интервал ожидания (аналогичный как в существующем sleep); 2) второй внутри первого, опрашивающий и обрабатывающий очереди событий (функционально аналогичный выполнению существующих коллбеков). 3. В тесте очередей учтен случай подключение читающего потока (№ 5) к обработке очередей в произвольный момент времени.
Код
// ============== Вариант реализации очередей событий QUIK (Lock-free, ~300 строк) ============
// Параметр Q_MAX определяет количество очередей в схеме очередей. Схем обработки очередей можно
// создать несколько.
// Данные, передаваемые в информационных очередях, строки.
// 1. Формат записи данных - текст описания таблицы Lua:
// {name = <Имя коллбека>, tbl = {<Таблица для формирования вызова функции пользователя>}}
// После чтения выполнять десереализацию текста таблицы Lua и дальше обрабатывать с учетом значения name.
// name обеспечивает возможность группировка коллбеков по очередям.
// 2. Коллбеки можно, но необязательно, сгруппировать и группы распределить по очередям с учетом того,
// что при чтении обработка начинается с 0-й очереди. Параметры очередей size_max следует задать
// такими, чтобы в них помещались строки их данных.
// Для коллбеков пользовательских таблиц QUIK, наверное, имеет смысл использовать все таки отдельную
// очередь.
// 3. Параметр FILLING_THRESHOLD - порог заполненности очередей для выдачи предупреждения о высоком уровне
// заполненности очередей.
// 4. После первого цикла любой очереди, память ее созданных элементов переиспользуется
// (кроме чтения указателя нет затрат на управление ее памятью).
// 5. Если для читающего потока в какой то его очереди возникнет переполнение, то об этом
// выдается сообщение.
// 6. Реализованы режимы записи в очереди: 0 - без сигналов (более эффективный); 1 - с сигналами.
// ----------------------------------------------------------------------
// Результат теста (оценка эффективности реализации очередей)
// Режим записи в очереди: без сигналов.
// Писатель выполняет в цикле запись в 10 очередей. Циклов записи : 1000
// T - время выполнения потоков с учетом их пауз в млс.
//-->Писатель. Пауза: 1. T (млс.): 1977. Обработано записей: 10000. Время ЦП (млс.): 3.4494
//Читатель 3. Пауза: 50. T (млс.): 2023. Обработано записей: 10000. Время ЦП (млс.): 1.3174
//Читатель 1. Пауза: 5. T (млс.): 2023. Обработано записей: 10000. Время ЦП (млс.): 1.1746
//Читатель 4. Пауза: 100. T (млс.): 2023. Обработано записей: 10000. Время ЦП (млс.): 1.2129
//Читатель 2. Пауза: 10. T (млс.): 2023. Обработано записей: 10000. Время ЦП (млс.): 1.2194
// -------------------------------------------------------------------------------
// Краткое ТЗ на разработку очередей
// 1) Создаются несколько циклических информационных очередей с разными типами данных и общим объектом
// управления ими.
// 2) В очереди пишет данные один поток, а читают эти очереди несколько потоков.
// 3) Дополнительно создается специальная служебная циклическая очередь: битовые шкалы непустых информационных
// очередей, записываемых пишущим потоком, после записи в информационные очереди.
// 4) У читающих потоков должны быть свои указатели чтения циклических очередей и их локальные битовые шкалы
// непустых очередей, хранящиеся в их объекте доступа к информационным очередям.
// 5) Читающие потоки циклически, выполнив свои коды переходят в состояние ожидания сигнала появления данных
// в их очередях или истечения заданного интервала времени на их общем объекте ожидания.
// 6) При записи в любую очередь пишущий поток после записи в информационные очереди записывает битовые шкалы
// непустых очередей в служебную очередь шкал непустых очередей и [активирует в режиме 1] все читающие потоки.
// 7) Каждый читающий поток, при пробуждении, читает все появившиеся записи в служебной очереди шкал непустых
// очередей и формирует свою локальную шкалу непустых очередей и далее, на ее основании, читает без
// синхронизации, непустые очереди с признаком : состояние прочитанных очередей в виде размера непрочитанных
// данных.
// 8) Реализовать эффективное управление памятью при передаче данных в этих очередях.
// C++20 (используется std::countr_zero) ---
#include <shlobj.h>
#include <windows.h>>
#include <iostream>
#include <vector>
#include <thread>
#include <chrono>
#include <mutex>
//#include <condition_variable>
//#include <atomic>
//#include <bitset>
//#include <intrin.h>
#pragma comment(lib, "winmm.lib") // Установить точность таймера 1 мс
// Параметры тестирования очередей ---
const int N_MAX = 2000; // Количество циклов записи при тестировании
//#define win32_API // В реализации очередей используется win32_API
const int QUEUE_RECORDING_MODE = 0; // Режим записи в очереди: 0 - без сигналов; 1 - с сигналами
const INT64 Q_SIZE = 64; // Длина информационной очереди по умолчанию
const INT64 Q_SIZE_SL = 256; // Длина служебной очереди шкал непустых информационных очередей
const int Q_MAX = 128; // Максимальное количество очередей
const int Количество_очередей = 120; // Количество используемых очередей ( <= Q_MAX)
const int LEN_STR_MAX = 4096; // Размер буфера потоков чтения (максимум size_max_str всех используемых очередей)
const double FILLING_THRESHOLD = 0.4; // Порог заполненности очередей для выдачи предупреждения об уровне заполненности очередей
// Системное время (синхронизированное). Точность 0,1 мкс.---
static INT64 T_OS_high_mls()
{
FILETIME lpSystemTimeAsFileTime;
GetSystemTimePreciseAsFileTime(&lpSystemTimeAsFileTime); // Системное время (синхронизированное)
INT64 tt = lpSystemTimeAsFileTime.dwHighDateTime;
tt = (tt << 32) + lpSystemTimeAsFileTime.dwLowDateTime;
return tt;
}
// Строка-объект информационной очереди (переиспользуемая) --
struct q_str {
INT64 tail = 0; // Указатель записи ---
INT64 size_max = 0; // Максимальный размер памяти строки (с учетом символа конца строки '\0')
INT64 size = 0; // Текущий размер памяти строки (с учетом символа конца строки '\0')
int n_q = 0; // Номер очереди
char* str = NULL; // Память строки (размер size_max) ----
int state = 0; // Состояние (не используется, но на всякий случай): 1 - занята пишущим потоком; 2 - в очереди.
};
// Параметры очереди, задаваемые при ее инициализации --
struct q_parm {
std::string name_Queue = ""; // имя очереди
INT64 q_size = 64; // длина очереди
INT64 size_max_str = 64; // максимальная память строки в информационной очереди
};
// Шаблонная циклическая очередь (Lock-free)
// Используется для служебной и информационных очередей
template<typename T>
class InfoQueue {
public:
std::vector<T> buffer;
INT64 tail = 0; // пишет только один писатель
std::string name_Queue = ""; // имя очереди (можно менять при начальной инициализации)
INT64 q_size = 64; // размер очереди (можно менять при начальной инициализации)
INT64 size_max_str = 64; // максимальная память строки в очереди (можно менять при начальной инициализации)
InfoQueue(INT64 s = Q_SIZE) : buffer(s), q_size(s) {}
// Изменение длины очереди ---
void q_size_set(int s) {
buffer.reserve(s); q_size = s;
}
// запись в очередь --
bool push(T val) {
buffer[tail++ % q_size] = val;
return true;
}
// Количества доступных элементов очереди для конкретного читателя
// local_head - указатель чтения (читающего потока)
// Если отрицательное значение, то: количество пропущенных при чтении записей очереди --
INT64 get_pending_count(INT64 local_head) const {
INT64 t = tail - local_head;
return (t <= q_size ? t : q_size - t);
}
};
// Копировать строку-объект в буфер читающего потока для последующей обработки ---
void q_str_copy(q_str* s, q_str* s1, InfoQueue<q_str*>* q)
{
//s1->size_max = s->size_max; // ! Задана при создании буфере --
s1->size = s->size;
s1->n_q = s->n_q;
if (s1->str == NULL) throw std::runtime_error("*** Ошибка: нет памяти для копирования");
// s1->size - размер памяти строки (с учетом символа конца строки '\0')
// Длина строки: (s1->size - 1)
memcpy(s1->str, s->str, s1->size); s1->tail = s->tail; // ! Обязательно в таком порядке --
s1->state = s->state;
// Дополнительный контроль переполнение очереди (после копирования в буфер потока чтения)
if ((q->tail - s1->tail) > q->q_size) std::cout << " *** Ошибка переполнение очереди (при копировании) " << s1->n_q << "\n";
}
// Объявления (определяются позже)
struct ReaderAccess;
struct Manager;
//-------------------------------
// Информационные очереди событий --
struct DataCluster {
// Деструктор (! выход из блока кода, в котором определен DataCluster
// только при завершения потоков, использующих его) --
~DataCluster() {
for (int i = 0; i < Q_MAX; ++i) {
INT64 size = q[i].buffer.size();
INT64 N = (q[i].tail >= size) ? size : q[i].tail; // Буфер может быть неполным ---
for (int j = 0; j < N; ++j) {
q_str* q_s = q[i].buffer[1];
delete[]q_s->str;
delete q_s;
}
}
}
int count_q = 64; // Количество используемых информационных очередей (<= Q_MAX) --
// При количестве очередей > 64 один бит шкалы непустых очередей соотносится
// к нескольким ((count_q - 1) / 64 + 1) последовательным очередям, в которых,
// возможно, есть записи.
UINT32 Queue_grouping = 1;
InfoQueue<q_str*> q[Q_MAX];
// Задать количество используемых очередей и их параметры
void set_parm_q(int n, q_parm* parm) {
if (n > 0 && n <= Q_MAX)
count_q = n;
else
throw std::runtime_error(" Количество используемых очередей > Q_MAX или <= 0"); /* #### ошибка*/
Queue_grouping = (count_q - 1) / 64 + 1;
if (parm != NULL)
for (int i = 0; i < count_q; ++i) {
q[i].name_Queue = parm->name_Queue;
q[i].q_size = parm->q_size;
q[i].q_size_set(parm->q_size);
q[i].size_max_str = parm->size_max_str;
}
}
// Функция формирования маски группировки очередей (многозначное отображение очередей на шкалу непустых)
uint64_t q_mask(int q_i) { return (uint64_t)1 << (q_i / Queue_grouping); }
// Запрос памяти потоком писателем под запись очереди --
// Запрос памяти системы и формирование строки-объекта -----
q_str* get_q_s(int q_i) {
INT64 size_max_str = q[q_i].size_max_str;
char* str = new char[size_max_str];
str[size_max_str - 1] = '\0';
q_str* q_s = new q_str;
q_s->str = str;
q_s->size_max = size_max_str;
q_s->size = 0;
q_s->n_q = q_i;
return q_s;
}
// Запрос строки-объекта потоком записи для формирования строки
q_str* get_str(int q_i) {
q_str* q_s;
INT64 tail = q[q_i].tail;
INT64 t = tail % q[q_i].q_size;
if (q[q_i].buffer[t] != NULL) { // Получение памяти из очереди --
q_s = q[q_i].buffer[t];
q_s->tail = tail;
}
else { // Запрос памяти у системы --
q_s = get_q_s(q_i);
q_s->tail = tail;
}
return q_s;
}
// Инициализация используемых очередей строками-объектами --
// #### Вряд ли стоит использовать ---
void initialization_q_str() {
if (count_q > 0) {
for (int i = 0; i < count_q; ++i) {
int size = q[i].q_size;
for (int j = 0; j < size; ++j) {
if (q[i].buffer[j] == NULL)
q[i].buffer[j] = get_q_s(i);
}
}
}
}
// Начальная инициализация указателей чтения в потоке-читателе на основе указателей
// записи в очереди (при начальной инициализации потока)
// Объявление.
void init_access(ReaderAccess* access, Manager* mgr, DataCluster* dc);
// запись в очередь --
bool push(int q_n, q_str* val) {
if (q_n < 0 || q_n >= count_q) throw std::runtime_error("*** Ошибка: нет такой очереди");
q[q_n].buffer[q[q_n].tail++ % q[q_n].q_size] = val;
return true;
}
};
//struct ReaderAccess; // Используется в Manager
#ifndef win32_API
// Объект управления потоками чтения
struct Manager {
INT64 signal_counter = 0; // Счетчик сигналов записи
int Выдавать_сигналы = 0; // 0 - без выдачи сигнала; 1 - выдача сигнала
InfoQueue<uint64_t> service_q{ Q_SIZE_SL }; // Служебная очередь битовых шкал
//--------- condition_variable --
std::mutex mtx; // Для синхронизации при ожидании
std::condition_variable cv; // Общий объект ожидания
void pulse(uint64_t mask) {
service_q.push(mask); ++signal_counter;
if (Выдавать_сигналы == 1)
cv.notify_all();
}
void wait(ReaderAccess* access, int tt); // !! Реализация после struct ReaderAccess (иначе недоступны поля)
};
#else // !win32_API
// Вариант 2. #### Этот вариант работает, но предыдущий лучше --
struct Manager {
INT64 signal_counter = 0; // Счетчик сигналов записи
int Выдавать_сигналы = 0; // 0 - без выдачи сигнала; 1 - выдача сигнала
InfoQueue<uint64_t> service_q{ Q_SIZE_SL }; // Служебная очередь битовых шкал
//--------- condition_variable --
CRITICAL_SECTION cs;
CONDITION_VARIABLE cv;
Manager() {
InitializeCriticalSection(&cs);
InitializeConditionVariable(&cv);
}
bool dataReady = false;
void pulse(uint64_t mask) {
service_q.push(mask);
++signal_counter;
EnterCriticalSection(&cs); // #### ?
if (Выдавать_сигналы == 1) {
dataReady = true;
WakeAllConditionVariable(&cv); // Будим тех, кто еще спит
}
LeaveCriticalSection(&cs); // #### ?
}
void wait(ReaderAccess* access, int tt); // !! Реализация после struct ReaderAccess (иначе недоступны поля)
};
#endif // !win32_API
// Объект доступа к очередям для читающих потоков --
struct ReaderAccess {
INT64 h[Q_MAX] = { 0 }; // Локальные указатели чтения очередей
INT64 h_service = 0; // Указатель чтения служебной очереди
uint64_t local_bitmask = 0; // Локальная шкала непустых очередей потока-читателя
INT64 signal_counter = 0; // Локальный счетчик сигналов записи
// -------- Статистика очередей ---------
INT64 NN[Q_MAX] = { 0 }; // Коичество обращений к непустым очередям
double statistics[Q_MAX] = { 0 }; // Сумма относительной заполненности очередей (среднее = statistics [i]/ (NN[i]))
double q_filling_threshold = 0.4; // Порог заполненности очередей для выдачи предупреждения --
int period_agr = 9; // Период скользящей
// ------- Данные снимка состояния очередей потока чтения ---
int q_len[Q_MAX] = { 0 }; // Количество непустых непрочитанных записей в очередях
int q_n[Q_MAX] = { 0 }; // Номера непустых очередей
int q_p = 0; // Длина векторов q_n и q_len
// Снимок состояния очередей потока-читателя ("мгновенный", чтобы меньше было повторных чтений очередей).
void snapshot_queue_state(DataCluster& dc, Manager& mgr) {
q_p = 0; // Сброс длины векторов q_n и q_len
// Формирование шкалы непустых очередей потока-читателя и снимка состояния очередей потока-читателя
INT64 count = mgr.service_q.get_pending_count(h_service);
if (count > 0) {
// Формируем общую локальную шкалу непустых очередей потока-читателя --
local_bitmask = 0;
uint64_t mask_val;
for (int i = 0; i < count; ++i) {
mask_val = mgr.service_q.buffer[h_service % mgr.service_q.q_size];
local_bitmask |= mask_val;
++h_service;
}
// Формирование снимка состояния очередей потока-читателя (на основе его шкалы) --
int pos_old = 0;
int Queue_grouping = dc.Queue_grouping;
while (local_bitmask) { // Эффективная обработка шкалы непустых очередей --
//// ! Способ определения позиции младшего разряда для старых стандартов (MSVC) -----
// unsigned long pos3 = 0;
// _BitScanForward64(&pos3, local_bitmask); // позиция младшего разряда с 1
//// -------------------------------------------------
int Сдвиг = std::countr_zero(local_bitmask); // C++20 (позиция младшего разряда с 1)
int pos = Сдвиг + pos_old;
pos_old = pos + 1;
pos *= Queue_grouping;
for (int i = 0; i < Queue_grouping; ++i) { // обработка групп очередей разряда шкалы --
INT64 q_count = dc.q[pos + i].get_pending_count(h[pos + i]);
if (q_count > 0) {
int p = q_p;
q_n[p] = pos + i;
q_len[p] = q_count;
// Статистика заполненности очереди (относительная, приблизительно средне скользящая)
if (++NN[pos + i] < period_agr) {
statistics[pos + i] += (double)q_count / (double)dc.q[pos + i].q_size;
}
else {
statistics[pos + i] += (double)q_count / (double)dc.q[pos + i].q_size - statistics[pos + i] / period_agr;
}
// --------------------------------------------------------------------------------
++q_p;
}
else
if (q_count < 0) std::cout << " *** Ошибка переполнение очереди (при чтении) q = " << pos + i << ": q_count = " << q_count << "\n"; // #### Ошибка переполнение очереди
}
local_bitmask >>= Сдвиг + 1;
}
}
else if (count < 0) std::cout << " *** -- Ошибка переполнение служебной очереди шкал --\n";
//std::cout << ". Шкала : " << std::bitset<64>(local_bitmask) << "\n";
}
// Чтение строки-объекта очереди --
// q_n - номер очереди
q_str* get_q_str(int q_n, DataCluster& dc) {
q_str* val = dc.q[q_n].buffer[h[q_n] % dc.q[q_n].q_size];
++h[q_n];
return val;
}
};
// ! Реализация члена DataCluster (расположена после struct ReaderAccess, Manager и DataCluster
// чтобы в методе были доступны их поля)
// Начальная инициализация указателей чтения в потоке-читателе на основе указателей
// записи в очереди (при начальной инициализации потока)
void DataCluster::init_access(ReaderAccess* access, Manager* mgr, DataCluster* dc) {
int count_q = dc->count_q;
if (count_q <= 0) throw std::runtime_error("*** Ошибка: не задано количество используемых очередей");
// Получение указателя служебной очереди
access->h_service = mgr->service_q.tail;
// Получение указателей информационных очередей
for (int i = 0; i < count_q; ++i) access->h[i] = q[i].tail;
}
// ! Реализация члена Manager (расположена после struct ReaderAccess чтобы в методе были
// доступны ее поля)
// Пауза
#ifndef win32_API
void Manager::wait(ReaderAccess* access, int dt) {
if (access->signal_counter == signal_counter) {// не было сигнала записи при отсутствии ожидания в потоке чтения
std::unique_lock<std::mutex> lock(mtx);
cv.wait_for(lock, std::chrono::milliseconds(2)) == std::cv_status::timeout;
}
access->signal_counter = signal_counter; // Обрабатываемый сигнал записи в потоке чтения
}
#else // !win32_API
// Вариант 2. #### -------
void Manager::wait(ReaderAccess* access, int dt) {
if (access->signal_counter == signal_counter) {// не было сигнала записи при отсутствии ожидания в потоке чтения
EnterCriticalSection(&cs);
// Пытаемся дождаться условия в течение заданного timeout
if (!dataReady) {
if (!SleepConditionVariableCS(&cv, &cs, 2)) {
if (GetLastError() == ERROR_TIMEOUT) {
//std::cout << "Thread [" << tt << "ms] TIMED OUT!" << std::endl;
}
}
else {
//std::cout << "Thread [" << tt << "ms] GOT SIGNAL!" << std::endl;
}
dataReady = false;
}
LeaveCriticalSection(&cs);
}
access->signal_counter = signal_counter; // Обрабатываемый сигнал записи в потоке чтения
}
#endif // !win32_API
// Тестирование очередей событий ------------------------------------------------
// Формат записываемых строк общий для всех очередей: тексты таблиц Lua (сереализация).
// Читающий поток выполняет десереализацию полученных строк и запуск Lua-функций обработки событий.
// ----------------------------------------------------
// Функция потока-писателя
void producer(DataCluster& dc, Manager& mgr) {
std::cout << "Писатель. Запись выполняется в 10 очередей. Циклов записи: " << N_MAX << std::endl;
std::cout << "T - время выполнения потоков с учетом их пауз." << std::endl;
// -------------------------------------
INT64 tt = T_OS_high_mls(); // Для подсчета времени ЦП --
INT64 TT = tt; // Для замера времени выполнения ---
INT64 TCP = 0; // Время ЦП ---
INT64 NRQ = 0; // Количество обработанных записей
int dt = 1;
int q_n_test = 0;
for (int i = 1; i <= N_MAX; ++i) {
tt = T_OS_high_mls() - tt;
TCP += tt;
//std::this_thread::sleep_for(std::chrono::milliseconds(dt)); // Пауза потока записи
Sleep(dt); // И так можно
tt = T_OS_high_mls();
// %%%%%% Подготовка данных для записи в очереди в виде C-строк
// Запись строк в очереди --
uint64_t mask = 0; // шкала непустых очередей
// Запись в разные очереди
// !! rec->size - размер памяти строки (с учетом символа конца строки '\0')
// Длина строки: (rec->size - 1)
const char* source = "{name = 'OnCleanUp',tbl = { ....] }";
int sz = strlen(source) + 1;
q_n_test %= 2;
int q_n_test1 = q_n_test * 4; // Для записи в разные очереди
q_str* rec = dc.get_str(0 + q_n_test1); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(0 + q_n_test1, rec)) mask |= dc.q_mask(0 + q_n_test1); ++NRQ;
rec = dc.get_str(1 + q_n_test1); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(1 + q_n_test1, rec)) mask |= dc.q_mask(1 + q_n_test1); ++NRQ;
rec = dc.get_str(2 + q_n_test1); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(2 + q_n_test1, rec)) mask |= dc.q_mask(2 + q_n_test1); ++NRQ;
rec = dc.get_str(3); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(3 + q_n_test1, rec)) mask |= dc.q_mask(3 + q_n_test1); ++NRQ;
++q_n_test;
rec = dc.get_str(40); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(40, rec)) mask |= dc.q_mask(40); ++NRQ;
rec = dc.get_str(41); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(41, rec)) mask |= dc.q_mask(41); ++NRQ;
rec = dc.get_str(40); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(40, rec)) mask |= dc.q_mask(40); ++NRQ;
rec = dc.get_str(41); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(41, rec)) mask |= dc.q_mask(41); ++NRQ;
rec = dc.get_str(42); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(42, rec)) mask |= dc.q_mask(42); ++NRQ;
rec = dc.get_str(100); memcpy(rec->str, "шшшшшш", 7); rec->size = 7;
if (i >= N_MAX) rec->state= -1; // Признак читателям завершить поток ---
if (dc.push(100, rec)) mask |= dc.q_mask(100); ++NRQ; // "шшшшшш"
// std::cout << "[P] Данные записаны, маска: " << std::bitset<64>(mask) << std::endl; // ####
// Запись маски и активация потоков
mgr.pulse(mask);
}
std::stringstream out; // можно использовать <<
out << "---> Писатель. Пауза:" << dt
<< ". T (млс.):"
<< (int)((T_OS_high_mls() - TT) * 0.0001) << ". Обработано записей:" << NRQ
<< ". Время ЦП (млс.):" << TCP * 0.0001 << "\n";
std::string result = out.str(); // Получаем std::string
std::cout << result;
}
// Заглушка функции обработки событий --
// Выполняет десереализацию полученных строк и запуск Lua-функций обработки событий
// в зависимости от name (вида очереди)
// 1) !! Память q_s возвращать не надо
// 2) !! q_s->size - размер памяти строки (с учетом символа конца строки '\0')
// Длина строки: (q_s->size - 1)
void event_handling(q_str* q_s, std::string name) {
}
// Функция потоков-читателей --
// filling_threshold - порог заполненности очередей (<текущее количество записей очереди> / <размер очереди>)
// для выдачи предупреждений: "Превышен порог заполненности очередей"
// dt - пауза (млс.)
void consumer(int id, DataCluster& dc, Manager& mgr, double filling_threshold, int dt) {
ReaderAccess access;
access.q_filling_threshold = filling_threshold;
// Инициализация access.h и access->h_service с учетом текущего состояния очередей
dc.init_access(&access, &mgr, &dc); // "Встраивание" потока чтения в очереди
// Буфер строки-объекта для дальнейшей обработки - q_str_c --
const char str_copy[LEN_STR_MAX] = {'\0'};
q_str q_str_c; q_str_c.str = (char*)str_copy; q_str_c.size_max = LEN_STR_MAX;
// -------------------------------------
INT64 tt = T_OS_high_mls();
INT64 TT = tt; // Для замера времени выполнения ---
dt = dt > 1 ? dt : 1;
INT64 DT = dt * 10000;
INT64 DTT = 0;
INT64 TCP = 0; // Для замера времени ЦП обработки очередей
INT64 NRQ = 0; // Количество обработанных записей
INT64 NACT = 0; // Количество активаций потока чтения
while (true) { // Пользовательский цикл обработки
tt = T_OS_high_mls();
DTT = T_OS_high_mls() + DT;
while ((T_OS_high_mls() - DTT) < 0) { // Цикл обработки очерелей событий
tt = (T_OS_high_mls() - tt); // время обработки потока в цикле --
TCP += tt;
// Пауза
mgr.wait(&access, dt);
tt = T_OS_high_mls();
++NACT;
// ------------------------------------------------------------------
// Получение снимка состояния очередей потока
access.snapshot_queue_state(dc, mgr);
// Обработка снимка состояния очередей потока:
// 1) access.q_p - вектор номеров непустых очередей
// 2) access.q_len - вектор длин необработанных очередей
// 3) access.q_p - длина векторов 1), 2)
int q_p = access.q_p;
// Просмотр и обработка вектора непустых очередей потока чтения
for (int i = 0; i < q_p; ++i) {
int q_n = access.q_n[i];
int count = access.q_len[i]; // Количество записей в очереди --
// ------- Сбор статистики ----
double statistics_agr = access.statistics[q_n] / (access.NN[q_n] < access.period_agr ? access.NN[q_n] : access.period_agr);
if (statistics_agr > access.q_filling_threshold) // #### Предупреждающее сообщение --
std::cout << "[Поток: " << id << "] Очередь "
<< q_n << ". Заполненность (средняя): " << statistics_agr
<< ". *** Превышен порог заполненности очередей: " << access.q_filling_threshold << "\n";
//// Отладочная печать
//if (count > 0 || count < 0)
// std::cout << "[Поток: " << id << "] Очередь "
// << q_n << ". Заполненность (средняя): " << statistics_agr
// << ". Количество: " << count << ". Ук чтения " << access.h[q_n] << "\n";
// --------------------
INT64 q_size = dc.q[q_n].q_size;
// Чтение и обработка очередей
while (count--) {
q_str* val_q = access.get_q_str(q_n, dc);
// Копировать запись очереди в буфер обработки q_str_c (типа q_str),
// созданный в стеке --
q_str_copy(val_q, &q_str_c, &dc.q[q_n]); val_q = NULL; // ! далее использовать только q_str_c
event_handling(&q_str_c, dc.q[q_n].name_Queue); // Вызов функции обработки записи очереди
++NRQ; // Количество обработанных записей
// Результат теста
// Завершение потока при тестировании --
if (q_str_c.state == -1) { // Признак читателям завершить поток ---
std::stringstream out; // можно использовать <<
out << "Читатель " << id << ". Пауза:" << dt
<< ". T (млс.):"
<< (int)((T_OS_high_mls() - TT) * 0.0001) << ". Обработано записей:" << NRQ
<< ". Циклов обработки:" << NACT
<< ". Время ЦП (млс.):" << TCP * 0.0001 << "\n";
std::string result = out.str(); // Получаем std::string
std::cout << result;
return;
}
// Отладочная печать
//const char* val_str = q_str_c.str;
//if (access.h[q_n] == N_MAX) {
// std::cout << " Поток " << id << ". Очередь " << q_n << " -> Val(str): " << val_str << ". Указатель чтения "
// << access.h[q_n] << ". Интервал (млс.) " << (T_OS_high_mls() - TT) * 0.0001 << "\n";
//}
}
}
}
//// %%%%%%%%%%%% Обработка остальных кодов (пользователя) в потоке чтения --
//if (dt > 50)
// std::cout << " Поток " << "\n";
}
}
// --------------------- Тест очередей -------------------------
int main() {
SetConsoleCP(1251); // Кодировка 1251 (! #include <shlobj.h>)
setlocale(LC_ALL, ""); // #### Русификация вывода ввода (работает) -----
timeBeginPeriod(1); // Установить точность 1 мс
// ==================================
#ifdef win32_API
std::cout << "В реализации используется win32_API" << std::endl;
#endif
DataCluster dc; // Очереди ---
// 1. Для каждой очереди должен быть задан размер памяти для максимальной строки, передаваемой
// в ней (с учетом признака конца строки '\0').
// 2. В вызове метода set_parm_q второй параметр: вектор типа q_parm * с параметрами очередей
// 3. Порядок элементов в векторе определяет приоритет обработки очередей (0 -> 1 ...)
q_parm* Параметры_очередей = NULL; // Параметры_очередей[Количество_очередей]
dc.set_parm_q(Количество_очередей, Параметры_очередей);
//dc.initialization_q_str(); // #### Вряд ли стоит использовать ---
Manager mgr; // Объект управления потоками чтения
mgr.Выдавать_сигналы = QUEUE_RECORDING_MODE; // Режим записи в очереди: 0 - без сигналов; 1 - с сигналами
std::cout << "Режим записи в очереди: " << (mgr.Выдавать_сигналы == 0 ? "без сигналов" : "с сигналами") << std::endl;
// Читающие потоки --
// Последний параметр интервал ожидания (млс.)
std::thread c1(consumer, 1, std::ref(dc), std::ref(mgr), FILLING_THRESHOLD, 5);
std::thread c2(consumer, 2, std::ref(dc), std::ref(mgr), FILLING_THRESHOLD, 10);
std::thread c3(consumer, 3, std::ref(dc), std::ref(mgr), FILLING_THRESHOLD, 50);
std::thread c4(consumer, 4, std::ref(dc), std::ref(mgr), FILLING_THRESHOLD, 100);
// Пишущий поток --
std::thread p(producer, std::ref(dc), std::ref(mgr));
// подключение читающего потока к обработке очередей в произвольный момент времени.
const int Задержка_запуска_читателя = 700;
std::cout << "Задержка_запуска_читателя 5 на " << Задержка_запуска_читателя << " млс." << std::endl;
Sleep(Задержка_запуска_читателя);
std::thread c5(consumer, 5, std::ref(dc), std::ref(mgr), FILLING_THRESHOLD, 1);
p.join(); c1.join(); c2.join(); c3.join(); c4.join(); c5.join();
timeEndPeriod(1); // Освободить точность 1 млс.
char nazv_sh[100];
std::cout << "Введите что-нибудь:" << std::endl;
std::cin >> nazv_sh;
return 0;
}
Превышено максимальное количество транзакций 50 в интервале 1 сек, Некорректная работа sendTransaction
Пользователь
Сообщений: Регистрация: 12.05.2020
13.03.2026 11:00:57
Цитата
Михаил Понамаренко написал: При ошибке "Превышено максимальное количество транзакций 50 в интервале 1 сек"Не возвращается описание ошибки через sendTransaction.
Я задавал такой же вопрос. Ссылка на диалог:
Гарантируется ли вызов колбэка при получении Квиком новых данных?, Вопросы разработчикам QUIK
Пользователь
Сообщений: Регистрация: 12.05.2020
11.03.2026 18:54:24
Цитата
paluke написал: То есть поставили вы в коллбеке sleep и данные не принимаются, новых событий нет.
И торги на бирже остановлены ?
Цитата
paluke написал: Так коллбеки в том же потоке, что и прием данных
Вы это проверяли?
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
11.03.2026 08:57:19
В классе InfoQueue, в методе
Цитата
TGB написал: // запись в очередь -- bool push(T val) { if (tail < q_size) buffer[tail % q_size] = val; ++tail; return true; }
Ошибка: строка с if лишняя. Правильно:
Код
// запись в очередь --
bool push(T val) {
buffer[tail % q_size] = val;
++tail;
return true;
}
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
09.03.2026 21:59:17
В соответствии с ТЗ (п.1,2, 4-8) ниже выложен код на C++ варианта реализации (Lock-free) циклических очередей событий QUIK. При использовании этого варианта в реализации п. 9-11 необходимости нет. 1. Блокировок между пишущим основным потоком QUIK и читающими потоками (main) нет. 2. Результата выполненных тестов эффективности приведены в комментариях кодов. В тестах накладные расходы времени ЦП: 1) на запись в очередь одного сообщения пишущим потоком ~0.007 мкс. 2) на чтение одного сообщения читающим потоком ~0.13 мкс. 3. Порог заполненности очередей обеспечивает выдачу предупреждения о высоком уровне заполненности очередей для читающих потоков. Если для читающего потока в какой то его очереди возникнет переполнение, то об этом выдается сообщение. 4. Независимо от задаваемой паузы ожидания в читающем потоке, он активируется всякий раз, при появлении новых записей в его очередях. Вычисляемая (реальная) пауза читающего потока = 'пауза ожидания' - 'время обработки его цикла (новых коллбеков и кодов пользователя)', но >= 1 млс. В выложенном коде устанавливается разрешение таймера = 1 млс. (вместо 16,...). 5. Все основные характеристики очередей параметризированы. Функция запроса свойств очередей в выложенном коде не реализована из-за ее простоты. 6. Выложенный вариант обеспечивает, при его использовании, сохранения существующего API QLua.
Код
// ============== Вариант реализации очередей событий QUIK (Lock-free, ~300 строк) ============
// Параметр Q_MAX определяет количество очередей в схеме очередей. Схем обработки очередей можно
// создать несколько.
// Данные, передаваемые в информационных очередях, строки.
// 1. Формат записи данных - текст описания таблицы Lua:
// {name = <Имя коллбека>, tbl = {<Таблица для формирования вызова функции пользователя>}}
// После чтения выполнять десереализацию текста таблицы Lua и дальше обрабатывать с учетом значения name.
// name обеспечивает возможность группировка коллбеков по очередям.
// 2. Коллбеки можно, но необязательно, сгруппировать и группы распределить по очередям с учетом того,
// что при чтении обработка начинается с 0-й очереди. Параметры очередей size_max следует задать
// такими, чтобы в них помещались строки их данных.
// Для коллбеков пользовательских таблиц QUIK, наверное, имеет смысл использовать все таки отдельную
// очередь.
// 3. Параметр FILLING_THRESHOLD - порог заполненности очередей для выдачи предупреждения о высоком уровне
// заполненности очередей.
// 4. После первого цикла любой очереди, память ее созданных элементов переиспользуется
// (кроме чтения указателя нет затрат на управление ее памятью).
// 5. Если для читающего потока в какой то его очереди возникнет переполнение, то об этом
// выдается сообщение.
// ----------------------------------------------------------------------
// Результат теста (оценка эффективности реализации очередей)
// Писатель выполняет в цикле запись в 10 очередей. Циклов записи : 1000
// T - время выполнения потоков с учетом их пауз в млс.
//-->Писатель. Пауза: 1. T (млс.): 1977. Обработано записей: 10000. Время ЦП (млс.): 0.0686
//Читатель 3. Пауза: 50. T (млс.): 2023. Обработано записей: 10000. Время ЦП (млс.): 1.3174
//Читатель 1. Пауза: 5. T (млс.): 2023. Обработано записей: 10000. Время ЦП (млс.): 1.1746
//Читатель 4. Пауза: 100. T (млс.): 2023. Обработано записей: 10000. Время ЦП (млс.): 1.2129
//Читатель 2. Пауза: 10. T (млс.): 2023. Обработано записей: 10000. Время ЦП (млс.): 1.2194
// -------------------------------------------------------------------------------
// Краткое ТЗ на разработку очередей
// 1) Создаются несколько циклических информационных очередей с разными типами данных и общим объектом
// управления ими.
// 2) В очереди пишет данные один поток, а читают эти очереди несколько потоков.
// 3) Дополнительно создается специальная служебная циклическая очередь: битовые шкалы непустых информационных
// очередей, записываемых пишущим потоком, после записи в информационные очереди.
// 4) У читающих потоков должны быть свои указатели чтения циклических очередей и их локальные битовые шкалы
// непустых очередей, хранящиеся в их объекте доступа к информационным очередям.
// 5) Читающие потоки циклически, выполнив свои коды переходят в состояние ожидания сигнала появления данных
// в их очередях или истечения заданного интервала времени на их общем объекте ожидания.
// 6) При записи в любую очередь пишущий поток после записи в информационные очереди записывает битовые шкалы
// непустых очередей в служебную очередь шкал непустых очередей и активирует все читающие потоки.
// 7) Каждый читающий поток, при активации, читает все появившиеся записи в служебной очереди шкал непустых
// очередей и формирует свою локальную шкалу непустых очередей и далее, на ее основании, читает без
// синхронизации, непустые очереди с признаком : состояние прочитанных очередей в виде размера непрочитанных
// данных.
// 8) Реализовать эффективное управление памятью при передаче данных в этих очередях.
// C++20 (используется std::countr_zero) ---
#include <shlobj.h>
#include <windows.h>>
#include <iostream>
#include <vector>
#include <thread>
#include <chrono>
#include <mutex>
//#include <condition_variable>
//#include <atomic>
//#include <bitset>
//#include <intrin.h>
#pragma comment(lib, "winmm.lib") // Установить точность таймера 1 мс
// Параметры тестирования очередей ---
const int N_MAX = 1000; // Количество циклов записи при тестировании
const INT64 Q_SIZE = 64; // Длина информационной очереди по умолчанию
const INT64 Q_SIZE_SL = 1024; // Длина служебной очереди шкал непустых информационных очередей
const int Q_MAX = 128; // Максимальное количество очередей
const int Количество_очередей = 120; // Количество используемых очередей ( <= Q_MAX)
const int LEN_STR_MAX = 4096; // Размер буфера потоков чтения (максимум size_max_str всех используемых очередей)
const double FILLING_THRESHOLD = 0.4; // Порог заполненности очередей для выдачи предупреждения об уровне заполненности очередей
// Системное время (синхронизированное). Точность 0,1 мкс.---
static INT64 T_OS_high_mls()
{
FILETIME lpSystemTimeAsFileTime;
GetSystemTimePreciseAsFileTime(&lpSystemTimeAsFileTime); // Системное время (синхронизированное)
INT64 tt = lpSystemTimeAsFileTime.dwHighDateTime;
tt = (tt << 32) + lpSystemTimeAsFileTime.dwLowDateTime;
return tt;
}
// Строка-объект информационной очереди (переиспользуемая) --
struct q_str {
INT64 tail = 0; // Указатель записи ---
INT64 size_max = 0; // Максимальный размер памяти строки (с учетом символа конца строки '\0')
INT64 size = 0; // Текущий размер памяти строки (с учетом символа конца строки '\0')
int n_q = 0; // Номер очереди
char* str = NULL; // Память строки (размер len_max) ----
int state = 0; // Состояние (не используется, но на всякий случай): 1 - занята пишущим потоком; 2 - в очереди.
};
// Параметры очереди, задаваемые при ее инициализации --
struct q_parm {
std::string name_Queue = ""; // имя очереди
INT64 q_size = 0; // длина очереди
INT64 size_max_str = 64; // максимальная память строки в информационной очереди
};
// Шаблонная циклическая очередь (Lock-free)
// Используется для информационных очередей и служебной
template<typename T>
class InfoQueue {
public:
std::vector<T> buffer;
INT64 tail = 0; // пишет только один писатель
std::string name_Queue = ""; // имя очереди (можно менять при начальной инициализации)
INT64 q_size = 64; // размер очереди (можно менять при начальной инициализации)
INT64 size_max_str = 64; // максимальная память строки в очереди (можно менять при начальной инициализации)
InfoQueue(INT64 s = Q_SIZE) : buffer(s), q_size(s) {}
// Изменение длины очереди ---
void q_size_set(int s) {
buffer.reserve(s); q_size = s;
}
// запись в очередь --
bool push(T val) {
if (tail < q_size)
buffer[tail % q_size] = val;
++tail;
return true;
}
// Количества доступных элементов для конкретного читателя
// local_head - указатель чтения (читающего потока)
// Если отрицательное значение, то: количество пропущенных при чтении записей очереди --
INT64 get_pending_count(INT64 local_head) const {
INT64 t = tail - local_head;
return (t <= q_size ? t : q_size - t);
}
};
// Копировать строку-объект в буфер читающего потока для последующей обработки ---
void q_str_copy(q_str* s, q_str* s1, InfoQueue<q_str*>* q)
{
//s1->size_max = s->size_max; // ! Задана при создании буфере --
s1->size = s->size;
s1->n_q = s->n_q;
if (s1->str == NULL) throw std::runtime_error("*** Ошибка: нет памяти для копирования");
// s1->size - размер памяти строки (с учетом символа конца строки '\0')
// Длина строки: (s1->size - 1)
memcpy(s1->str, s->str, s1->size); s1->tail = s->tail; // ! Обязательно в таком порядке --
s1->state = s->state;
// Дополнительный контроль переполнение очереди (после копирования в буфер потока чтения)
if ((q->tail - s1->tail) > q->q_size) std::cout << " *** Ошибка переполнение очереди (при копировании) " << s1->n_q << "\n";
}
// Информационные очереди событий --
struct DataCluster {
// Деструктор (! выход из блока кода, в котором определен DataCluster
// только при завершения потоков, использующих его) --
~DataCluster() {
for (int i = 0; i < Q_MAX; ++i) {
INT64 size = q[i].buffer.size();
INT64 N = (q[i].tail >= size) ? size : q[i].tail; // Буфер может быть неполным ---
for (int j = 0; j < N; ++j) {
q_str* q_s = q[i].buffer[1];
delete[]q_s->str;
delete q_s;
}
}
}
int count_q = 64; // Количество используемых информационных очередей (<= Q_MAX) --
// При количестве очередей > 64 один бит шкалы непустых очередей соотносится
// к нескольким ((count_q - 1) / 64 + 1) последовательным очередям, в которых,
// возможно, есть записи.
UINT32 Queue_grouping = 1;
InfoQueue<q_str*> q[Q_MAX];
// Задать количество используемых очередей и их параметры
void set_parm_q(int n, q_parm* parm) {
if (n > 0 && n <= Q_MAX)
count_q = n;
else
throw std::runtime_error(" Количество используемых очередей > Q_MAX или <= 0"); /* #### ошибка*/
Queue_grouping = (count_q - 1) / 64 + 1;
if (parm != NULL)
for (int i = 0; i < count_q; ++i) {
q[i].name_Queue = parm->name_Queue;
q[i].q_size = parm->q_size;
q[i].q_size_set(parm->q_size);
q[i].size_max_str = parm->size_max_str;
}
}
// Функция формирования маски группировки очередей (многозначное отображение очередей на шкалу непустых)
uint64_t q_mask(int q_i) { return (uint64_t)1 << (q_i / Queue_grouping); }
// Запрос памяти потоком писателем под запись очереди --
// Запрос памяти системы и формирование строки-объекта -----
q_str* get_q_s(int q_i) {
INT64 size_max_str = q[q_i].size_max_str;
char* str = new char[size_max_str];
str[size_max_str - 1] = '\0';
q_str* q_s = new q_str;
q_s->str = str;
q_s->size_max = size_max_str;
q_s->size = 0;
q_s->n_q = q_i;
return q_s;
}
// Запрос строки-объекта потоком записи для формирования строки
q_str* get_str(int q_i) {
q_str* q_s;
INT64 tail = q[q_i].tail;
INT64 t = tail % q[q_i].q_size;
if (q[q_i].buffer[t] != NULL) { // Получение памяти из очереди --
q_s = q[q_i].buffer[t];
q_s->tail = tail;
}
else { // Запрос памяти у системы --
q_s = get_q_s(q_i);
q_s->tail = tail;
}
return q_s;
}
// Инициализация используемых очередей строками-объектами --
// #### Вряд ли стоит использовать ---
void initialization_q_str() {
if (count_q > 0) {
for (int i = 0; i < count_q; ++i) {
int size = q[i].q_size;
for (int j = 0; j < size; ++j) {
if (q[i].buffer[j] == NULL)
q[i].buffer[j] = get_q_s(i);
}
}
}
}
// Начальная инициализация указателей чтения в потоке-читателе на основе указателей
// записи в очереди (при начальной инициализации потока)
void init_access(INT64 *tail_v) {
if (count_q <= 0) throw std::runtime_error("*** Ошибка: не задано количество используемых очередей");
for (int i = 0; i < count_q; ++i) tail_v[i] = q[i].tail;
}
// запись в очередь --
bool push(int q_n, q_str* val) {
if (q_n >= count_q) throw std::runtime_error("*** Ошибка: нет такой очереди");
if (q[q_n].tail < q[q_n].q_size)
q[q_n].buffer[q[q_n].tail % q[q_n].q_size] = val;
++q[q_n].tail;
return true;
}
};
struct ReaderAccess; // Используется в Manager
// Объект управления потоками чтения
struct Manager {
INT64 signal_counter = 0; // Счетчик сигналов записи
std::mutex mtx; // Для синхронизации при ожидании
std::condition_variable cv; // Общий объект ожидания
InfoQueue<uint64_t> service_q{ Q_SIZE_SL }; // Служебная очередь битовых шкал
void wait(ReaderAccess* access, int tt); // !! Реализация после struct ReaderAccess (иначе недоступны поля)
void pulse(uint64_t mask) { service_q.push(mask); ++signal_counter; cv.notify_all(); }
//// ------- win32 API --
//HANDLE cv = CreateEvent(NULL, FALSE, FALSE, NULL); // Общий объект ожидания
//void pulse() { ++signal_counter; PulseEvent(cv); }
private:
INT64 tt = T_OS_high_mls(); // Для коррекции интервала ожидания --
};
// Объект доступа к очередям для читающих потоков --
struct ReaderAccess {
INT64 h[Q_MAX] = { 0 }; // Локальные указатели чтения
INT64 h_service = 0; // Указатель чтения служебной очереди
uint64_t local_bitmask = 0; // Локальная шкала непустых очередей потока-читателя
INT64 signal_counter = 0; // Локальный счетчик сигналов записи
// -------- Статистика очередей ---------
INT64 NN[Q_MAX] = { 0 }; // Коичество обращений к непустым очередям
double statistics[Q_MAX] = { 0 }; // Сумма относительной заполненности очередей (среднее = statistics [i]/ (NN[i]))
double q_filling_threshold = 0.4; // Порог заполненности очередей для выдачи предупреждения --
int period_agr = 9; // Период скользящей
// ------- Данные снимка состояния очередей потока чтения ---
int q_len[Q_MAX] = { 0 }; // Количество непустых непрочитанных записей в очередях
int q_n[Q_MAX] = { 0 }; // Номера непустых очередей
int q_p = 0; // Длина векторов q_n и q_len
// Снимок состояния очередей потока-читателя ("мгновенный", чтобы меньше было повторных чтений очередей).
void snapshot_queue_state(DataCluster& dc, Manager& mgr) {
q_p = 0; // Сброс длины векторов q_n и q_len
// Формирование шкалы непустых очередей потока-читателя
uint64_t mask_val;
INT64 count = mgr.service_q.get_pending_count(h_service);
if (count > 0) {
// Формируем общую локальную шкалу непустых очередей потока-читателя --
for (int i = 0; i < count; ++i) {
mask_val = mgr.service_q.buffer[h_service % mgr.service_q.q_size];
local_bitmask |= mask_val;
++h_service;
}
// Формирование снимка состояния очередей потока-читателя (на основе его шкалы) --
int pos_old = 0;
int Queue_grouping = dc.Queue_grouping;
while (local_bitmask) { // Эффективная обработка шкалы непустых очередей --
//// ! Способ определения позиции младшего разряда для старых стандартов (MSVC) -----
// unsigned long pos3 = 0;
// _BitScanForward64(&pos3, local_bitmask); // позиция младшего разряда с 1
//// -------------------------------------------------
int Сдвиг = std::countr_zero(local_bitmask); // C++20 (позиция младшего разряда с 1)
int pos = Сдвиг + pos_old;
pos_old = pos + 1;
pos *= Queue_grouping;
for (int i = 0; i < Queue_grouping; ++i) { // обработка групп очередей разряда шкалы --
INT64 q_count = dc.q[pos + i].get_pending_count(h[pos + i]);
if (q_count > 0) {
int p = q_p;
q_n[p] = pos + i;
q_len[p] = q_count;
// Статистика заполненности очереди (относительная, приблизительно средне скользящая)
if (++NN[pos + i] < period_agr) {
statistics[pos + i] += (double)q_count / (double)dc.q[pos + i].q_size;
}
else {
statistics[pos + i] += (double)q_count / (double)dc.q[pos + i].q_size - statistics[pos + i] / period_agr;
}
++q_p;
}
else
if (q_count < 0) std::cout << " *** Ошибка переполнение очереди (при чтении) q = " << pos + i << ": q_count = " << q_count << "\n"; // #### Ошибка переполнение очереди
}
local_bitmask >>= Сдвиг + 1;
}
}
else if (count < 0) std::cout << " *** -- Ошибка переполнение служебной очереди шкал --\n";
//std::cout << ". Шкала : " << std::bitset<64>(local_bitmask) << "\n";
}
// Чтение строки-объекта очереди --
// q_n - номер очереди
q_str* get_q_str(int q_n, DataCluster& dc) {
q_str* val = dc.q[q_n].buffer[h[q_n] % dc.q[q_n].q_size];
++h[q_n];
return val;
}
};
// ! Реализация члена Manager (расположена после struct ReaderAccess чтобы в методе были
// доступны ее поля)
// Пауза (подстраиваемая с учетом длительности цикла обработки)
void Manager::wait(ReaderAccess* access, int dt) {
std::unique_lock<std::mutex> lock(mtx);
if (access->signal_counter == signal_counter) {// не было сигнала записи при отсутствии ожидания в потоке чтения
// Вычисление коррекции паузы с учетом времени обработки цикла --
tt = (T_OS_high_mls() - tt) * 0.0001; // время обработки потока в цикле (млс.) --
tt = (dt - tt);
tt = tt > 0 ? tt : 1;
cv.wait_for(lock, std::chrono::milliseconds(tt)) == std::cv_status::timeout;
//WaitForSingleObject(cv, tt); // вместо предыдущего оператора с комментированием первого оператора метода
}
tt = T_OS_high_mls();
access->signal_counter = signal_counter; // Обрабатываемый сигнал записи в потоке чтения
}
// Тестирование очередей событий ------------------------------------------------
// Функция потока-писателя
// Формат записываемых строк общий для всех очередей: тексты таблиц Lua (сереализация).
// Читающий поток выполняет десереализацию полученных строк и запуск Lua-функций обработки событий
void producer(DataCluster& dc, Manager& mgr) {
std::cout << "Писатель. Запись выполняется в 10 очередей. Циклов записи: " << N_MAX << std::endl;
std::cout << "T - время выполнения потоков с учетом их пауз." << std::endl;
// -------------------------------------
INT64 tt = T_OS_high_mls(); // Для коррекции интервала ожидания --
INT64 TT = tt; // Для замера времени выполнения ---
INT64 TCP = 0; // Для замера времени ЦП ---
INT64 NRQ = 0; // Количество обработанных записей
int dt = 1;
for (int i = 1; i <= N_MAX; ++i) {
tt = T_OS_high_mls() - tt;
TCP += tt;
std::this_thread::sleep_for(std::chrono::milliseconds(dt)); // Пауза потока записи
//Sleep(dt); // И так можно
// %%%%%% Подготовка данных для записи в очереди в виде C-строк
// Запись строк в очереди --
uint64_t mask = 0; // шкала непустых очередей
//// Запись в разные очереди
// !! rec->size - размер памяти строки (с учетом символа конца строки '\0')
// Длина строки: (rec->size - 1)
const char* source = "{name = 'OnCleanUp',tbl = { ....] }"; int sz = strlen(source) + 1;
q_str* rec = dc.get_str(0); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(0, rec)) mask |= dc.q_mask(0); ++NRQ;
rec = dc.get_str(1); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(1, rec)) mask |= dc.q_mask(1); ++NRQ;
rec = dc.get_str(2); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(2, rec)) mask |= dc.q_mask(2); ++NRQ;
rec = dc.get_str(3); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(3, rec)) mask |= dc.q_mask(3); ++NRQ;
rec = dc.get_str(40); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(40, rec)) mask |= dc.q_mask(40); ++NRQ;
rec = dc.get_str(41); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(41, rec)) mask |= dc.q_mask(41); ++NRQ;
rec = dc.get_str(40); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(40, rec)) mask |= dc.q_mask(40); ++NRQ;
rec = dc.get_str(41); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(41, rec)) mask |= dc.q_mask(41); ++NRQ;
rec = dc.get_str(42); memcpy(rec->str, source, sz); rec->size = sz;
if (dc.push(42, rec)) mask |= dc.q_mask(42); ++NRQ;
rec = dc.get_str(100); memcpy(rec->str, "шшшшшш", 7); rec->size = 7;
if (i >= N_MAX) rec->state= -1; // Признак читателям завершить поток ---
if (dc.push(100, rec)) mask |= dc.q_mask(100); ++NRQ; // "шшшшшш"
// std::cout << "[P] Данные записаны, маска: " << std::bitset<64>(mask) << std::endl; // ####
// Запись маски и активация потоков
mgr.pulse(mask);
tt = T_OS_high_mls();
}
std::stringstream out; // можно использовать <<
out << "---> Писатель. Пауза:" << dt
<< ". T (млс.):"
<< (int)((T_OS_high_mls() - TT) * 0.0001) << ". Обработано записей:" << NRQ
<< ". Время ЦП (млс.):" << TCP * 0.0001 << "\n";
std::string result = out.str(); // Получаем std::string
std::cout << result;
}
// Заглушка функции обработки событий.
// Выполняет десереализацию полученных строк и запуск Lua-функций обработки событий
// в зависимости от name (вида очереди)
// 1) !! Память q_s возвращать не надо
// 2) !! q_s->size - размер памяти строки (с учетом символа конца строки '\0')
// Длина строки: (q_s->size - 1)
void event_handling(q_str* q_s, std::string name) {
}
// Функция потоков-читателей --
// filling_threshold - порог заполненности очередей (<текущее количество записей очереди> / <размер очереди>)
// для выдачи предупреждений: "Превышен порог заполненности очередей"
// dt - пауза (млс.)
void consumer(int id, DataCluster& dc, Manager& mgr, double filling_threshold, int dt) {
ReaderAccess access;
access.q_filling_threshold = filling_threshold;
dc.init_access(access.h); // Инициализация access.h (указателей) потока чтения с учетом текущего состояния очередей --
// Буфер строки-объекта для дальнейшей обработки - q_str_c --
const char str_copy[LEN_STR_MAX] = {'\0'};
q_str q_str_c; q_str_c.str = (char*)str_copy; q_str_c.size_max = LEN_STR_MAX;
// -------------------------------------
INT64 tt = T_OS_high_mls(); // Для коррекции интервала ожидания --
INT64 TT = tt; // Для замера времени выполнения ---
INT64 TCP = 0; // Для замера времени ЦП ---
INT64 NRQ = 0; // Количество обработанных записей
while (true) {
tt = (T_OS_high_mls() - tt); // время обработки потока в цикле --
TCP += tt;
// Пауза (подстраиваемая с учетом длительности цикла обработки)
// с ожиданием сигнала записи в одну из информационных очередей --
mgr.wait(&access, dt);
tt = T_OS_high_mls();
// ------------------------------------------------------------------
// Получение снимка состояния очередей потока ("мгновенный",
// чтобы меньше было повторных чтений очередей).
access.snapshot_queue_state(dc, mgr);
// Обработка снимка состояния очередей потока:
// 1) access.q_p - вектор номеров непустых очередей
// 2) access.q_len - вектор длин необработанных очередей
// 3) access.q_p - длина векторов 1), 2)
int q_p = access.q_p;
// Просмотр и обработка вектора непустых очередей потока чтения
for (int i = 0; i < q_p; ++i) {
int q_n = access.q_n[i];
int count = access.q_len[i]; // Количество записей в очереди --
// ------- Сбор статистики ----
double statistics_agr = access.statistics[q_n] / (access.NN[q_n] < access.period_agr ? access.NN[q_n] : access.period_agr);
if (statistics_agr > access.q_filling_threshold) // #### Предупреждающее сообщение --
std::cout << "[Поток: " << id << "] Очередь "
<< q_n << ". Заполненность (средняя): " << statistics_agr
<< ". *** Превышен порог заполненности очередей: " << access.q_filling_threshold << "\n";
//// Отладочная печать
//if (count > 0 || count < 0)
// std::cout << "[Поток: " << id << "] Очередь "
// << q_n << ". Заполненность (средняя): " << statistics_agr
// << ". Количество: " << count << ". Ук чтения " << access.h[q_n] << "\n";
// --------------------
INT64 q_size = dc.q[q_n].q_size;
// Чтение и обработка очередей
while (count--) {
q_str* val_q = access.get_q_str(q_n, dc);
// Копировать запись очереди в буфер обработки q_str_c (типа q_str),
// созданный в стеке --
q_str_copy(val_q, &q_str_c, &dc.q[q_n]); val_q = NULL; // ! далее использовать только q_str_c
event_handling(&q_str_c, dc.q[q_n].name_Queue); // Вызов функции обработки записи очереди
++NRQ; // Количество обработанных записей
// Результат теста
// Завершение потока при тестировании --
if (q_str_c.state == -1) { // Признак читателям завершить поток ---
std::stringstream out; // можно использовать <<
out << "Читатель " << id << ". Пауза:" << dt
<< ". T (млс.):"
<< (int)((T_OS_high_mls() - TT) * 0.0001) << ". Обработано записей:" << NRQ
<< ". Время ЦП (млс.):" << TCP * 0.0001 << "\n";
std::string result = out.str(); // Получаем std::string
std::cout << result;
return;
}
// Отладочная печать
//const char* val_str = q_str_c.str;
//if (access.h[q_n] == N_MAX) {
// std::cout << " Поток " << id << ". Очередь " << q_n << " -> Val(str): " << val_str << ". Указатель чтения "
// << access.h[q_n] << ". Интервал (млс.) " << (T_OS_high_mls() - TT) * 0.0001 << "\n";
//}
// ----------------------------------------------------
//// %%%%%%%%%%%% Обработка остальных данных в потоке чтения --
}
}
}
}
// --------------------- Тест очередей -------------------------
int main() {
SetConsoleCP(1251); // Кодировка 1251 (! #include <shlobj.h>)
setlocale(LC_ALL, ""); // #### Русификация вывода ввода (работает) -----
timeBeginPeriod(1); // Установить точность 1 мс
// ==================================
DataCluster dc; // Очереди ---
// 1. Для каждой очереди должен быть задан размер памяти для максимальной строки, передаваемой
// в ней (с учетом признака конца строки '\0').
// 2. В вызове метода set_parm_q второй параметр: вектор типа q_parm * с параметрами очередей
// 3. Порядок элементов в векторе определяет приоритет обработки очередей (0 -> 1 ...)
q_parm* Параметры_очередей = NULL; // Параметры_очередей[Количество_очередей]
dc.set_parm_q(Количество_очередей, Параметры_очередей);
//dc.initialization_q_str(); // #### Вряд ли стоит использовать ---
Manager mgr; // Объект управления потоками чтения
// Читающие потоки --
// Последний параметр интервал ожидания (млс.)
std::thread c1(consumer, 1, std::ref(dc), std::ref(mgr), FILLING_THRESHOLD, 5);
std::thread c2(consumer, 2, std::ref(dc), std::ref(mgr), FILLING_THRESHOLD, 10);
std::thread c3(consumer, 3, std::ref(dc), std::ref(mgr), FILLING_THRESHOLD, 50);
std::thread c4(consumer, 4, std::ref(dc), std::ref(mgr), FILLING_THRESHOLD, 100);
// Пишущий поток --
std::thread p(producer, std::ref(dc), std::ref(mgr));
p.join(); c1.join(); c2.join(); c3.join(); c4.join();
timeEndPeriod(1); // Освободить точность
char nazv_sh[100];
std::cout << "Введите что-нибудь:" << std::endl;
std::cin >> nazv_sh;
return 0;
}
Гарантируется ли вызов колбэка при получении Квиком новых данных?, Вопросы разработчикам QUIK
Пользователь
Сообщений: Регистрация: 12.05.2020
06.03.2026 11:01:04
Цитата
Anton Belonogov написал: Подтверждаем: при получении терминалом новой записи или обновления существующей гарантируется вызов соответствующей функции обратного вызова.
Здравствуйте. Есть скорость v1 (количество/сек.) появления событий, по которым должны запускаться коллбеки. И есть скорость выполнения коллбеков v2 (количество/сек.) в скриптах пользователя. Пока v1 <= v2 можно как то, наверное, гарантировать. Но скоростью v2 QUIK не управляет. Например, пользователь в тексте коллбека, по недоразумению, написал строку: sleep(3000) и на 3 сек. все коллбеки, во всех выполняемых скриптах перестали обрабатываться. Или выполняет долгую обработку в коллбеке. Единственное, что можно бы было гарантировать в этих условиях - это выдачу сообщения о ситуации, когда v1 > v2. Но я таких сообщений никогда не видел (даже в экспериментах). Или я что то не понимаю?
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Согласен. В вашем конкретном случае достаточно первого варианта.
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
01.03.2026 11:28:58
Вариант ТЗ на модификацию интерфейса QLua c QUIK 1. Вместо существующей схемы запуска коллбеков создаются циклические очереди (соответствующие коллбекам). 2. В очереди пишет данные событий служебный поток QUIK, а читают эти очереди и выполняют функции их обработки потоки main скриптов пользователя в теле модифицированной функции sleep. 3. В меню, добавленное в окно запуска пользовательских скриптов, обеспечить возможность изменения пользователем умолчания параметров очередей, а также функций фильтрации данных, записываемых в очереди . 4. У читающих потоков должны быть свои указатели чтения циклических очередей и битовые шкалы непустых очередей, хранящиеся в объекте доступа к их очередям. 5. Читающие потоки циклически, выполнив свои коды переходят в состояние ожидания сигнала появления данных в их очередях или истечения заданного интервала времени. 6. При записи в любую очередь пишущий поток меняет состояние битовых шкал непустых очередей и активирует все читающие потоки. 7. Каждый читающий поток при чтении очереди, выбираемой на основании своей битовой шкалы непустых очередей, получает признак: состояние прочитанных очередей в виде размера непрочитанных данных. 8. Добавить в код динамическое управление количеством очередей и реализовать полностью Lock-Free механизм уведомления без использования std::mutex. 9. Добавить логику удаления очередей «на лету» (dynamic removal) с использованием механизма безопасного освобождения памяти, такого как Hazard Pointers. 10. Реализовать идиому Hazard Pointers с очередью на удаление (retire list). 11. Реализовать пользовательскую функцию запроса свойств очередей. 12. Подключать в QUIKе коды Lua без внесения изменений в его исходники. 13. Перенести все необходимые функции (с внесением изменений, учитывающих п.1 - 12), реализованные в текущей версии QUIK в исходниках Lua, в пакет dll. ------- Что в написанном выше не понятно? В моих сообщениях 91 и 92 приведены "сырые", но работающие прототипы, частично реализующие это ТЗ. На возражения, замечания и предложения по улучшению ТЗ, постараюсь ответить.
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
28.02.2026 15:33:43
Мне стало интересно, а более продвинутый вариант циклических очередей ИИ сможет реализовать? Промт для ИИ Написать программу на C++, в которой: 1) Создаются несколько циклических очередей с разными типами данных. 2) В очереди пишет данные один поток, а читают эти очереди несколько потоков. 3) У читающих потов должны быть свои указатели чтения циклических очередей и битовые шкалы непустых очередей, хранящиеся в объекте доступа к их очередям. 4) При записи в любую очередь пишущий поток меняет состояние битовых шкал непустых очередей и активирует все читающие потоки. 5) Каждый читающий поток при чтении очереди, выбираемой на основании своей битовой шкалы непустых очередей, получает признак: состояние прочитанных очередей в виде размера непрочитанных данных. 6) Добавить в код динамическое управление количеством очередей и реализовать полностью Lock-Free механизм уведомления без использования std::mutex.
Смог (все комментарии ИИ):
Код
// Это сложная задача, требующая использования атомарных операций (std::atomic) и барьеров памяти для реализации Lock-Free взаимодействия.
// Для выполнения условия динамического управления очередями и уведомления без мьютексов, мы будем использовать std::atomic_flag или
// std::atomic<bool> для сигнализации (busy-wait или wait/notify в C++20) и атомарные указатели.
// Ниже представлен концептуальный код, реализующий эту логику:
// C++20 ----
#include <iostream>
#include <vector>
#include <atomic>
#include <thread>
#include <memory>
#include <optional>
// Шаблонная циклическая очередь (Lock-Free для одного писателя и нескольких читателей со своими индексами)
template<typename T, size_t Capacity>
class CircularQueue {
T buffer[Capacity];
std::atomic<size_t> head{0}; // Указатель записи
public:
void push(T item) {
size_t h = head.load(std::memory_order_relaxed);
buffer[h % Capacity] = item;
head.store(h + 1, std::memory_order_release);
}
bool try_pop(size_t& reader_idx, T& out_item, size_t& pending_count) {
size_t current_head = head.load(std::memory_order_acquire);
if (reader_idx < current_head) {
out_item = buffer[reader_idx % Capacity];
reader_idx++;
pending_count = current_head - reader_idx;
return true;
}
pending_count = 0;
return false;
}
};
// Объект доступа для каждого читающего потока
struct ReaderContext {
std::atomic<uint64_t> bitmask{0}; // Битовая шкала непустых очередей
std::vector<size_t> read_indices; // Свои указатели чтения
std::atomic<bool> data_ready{false};
void update_mask(size_t queue_idx) {
bitmask.fetch_or(1ULL << queue_idx, std::memory_order_release);
data_ready.store(true, std::memory_order_release);
data_ready.notify_all(); // C++20 Lock-free уведомление
}
};
class QueueManager {
CircularQueue<int, 1024> int_queue;
CircularQueue<float, 1024> float_queue;
std::vector<std::shared_ptr<ReaderContext>> readers;
public:
void add_reader(std::shared_ptr<ReaderContext> ctx) {
ctx->read_indices.resize(2, 0); // 0 для int, 1 для float
readers.push_back(ctx);
}
void write_int(int val) {
int_queue.push(val);
for (auto& r : readers) r->update_mask(0);
}
void write_float(float val) {
float_queue.push(val);
for (auto& r : readers) r->update_mask(1);
}
void reader_thread_func(std::shared_ptr<ReaderContext> ctx, int id) {
while (true) {
// Ожидание данных (Lock-free wait)
ctx->data_ready.wait(false, std::memory_order_acquire);
uint64_t mask = ctx->bitmask.load(std::memory_order_acquire);
for (size_t i = 0; i < 2; ++i) {
if (mask & (1ULL << i)) {
size_t pending = 0;
if (i == 0) {
int val;
while (int_queue.try_pop(ctx->read_indices[0], val, pending)) {
printf("[Reader %d] Int: %d (Pending: %zu)\n", id, val, pending);
}
} else {
float val;
while (float_queue.try_pop(ctx->read_indices[1], val, pending)) {
printf("[Reader %d] Float: %.2f (Pending: %zu)\n", id, val, pending);
}
}
// Сбрасываем бит, если данных больше нет
if (pending == 0) ctx->bitmask.fetch_and(~(1ULL << i));
}
}
ctx->data_ready.store(false, std::memory_order_relaxed);
}
}
};
int main() {
QueueManager manager;
auto r1 = std::make_shared<ReaderContext>();
auto r2 = std::make_shared<ReaderContext>();
manager.add_reader(r1);
manager.add_reader(r2);
std::thread t1(&QueueManager::reader_thread_func, &manager, r1, 1);
std::thread t2(&QueueManager::reader_thread_func, &manager, r2, 2);
// Пишущий поток
std::thread writer([&]() {
for (int i = 0; i < 5; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
manager.write_int(100 + i);
manager.write_float(1.5f * i);
}
});
writer.join();
t1.detach();
t2.detach();
return 0;
}
// Ключевые моменты реализации :
// Индивидуальные индексы : Каждый читатель хранит свои read_indices, что позволяет им двигаться по одной и
// той же очереди независимо.
// Битовая шкала : Используется std::atomic<uint64_t>, где каждый бит соответствует ID очереди.Это позволяет
// мгновенно проверить, где появились данные.
// Lock - Free Notify : Использован метод atomic::wait / notify_all(доступен с C++20).Он работает эффективнее
// обычного yield, так как позволяет потоку "заснуть" без использования мьютекса в пользовательском
// коде(на уровне ОС это может использовать фьютексы).
// Pending Count : Метод try_pop возвращает разницу между указателем записи и текущим индексом чтения,
// выполняя условие получения "размера непрочитанных данных".
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
28.02.2026 10:42:54
Цитата
Oleg Kuzembaev написал: необходимо достичь понимания, какая именно доработка требуется.Если у вас уже есть такая наготове, то просьба предметно описать ее: что именно стоит добавить? Как это будет выглядеть в вашем представлении?
Промт для ИИ Написать программу на C++, в которой: 1) Создаются несколько циклических очередей с разными типами данных. 2) В очереди пишет данные один поток, а читают эти очереди несколько потоков. 3) У читающих потов должны быть свои указатели чтения циклических очередей. 4) При записи в любую очередь пишущий поток активирует все читающие потоки. 5) Каждый читающий поток при чтении получает признак: состояние прочитанных очередей в виде размера непрочитанных данных. -------- Результат от ИИ, полученный в течении 5 сек.(компилируемый, код-основа для разработки очередей событий):
Код
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
template <typename T, size_t Size>
class CircularQueue {
public:
void push(T item) {
buffer[head % Size] = item;
head.fetch_add(1, std::memory_order_release);
}
// Каждый читатель передает свой локальный индекс
bool try_read(size_t& reader_idx, T& out_item, size_t& pending_count) {
size_t current_head = head.load(std::memory_order_acquire);
if (reader_idx < current_head) {
out_item = buffer[reader_idx % Size];
reader_idx++;
pending_count = current_head - reader_idx;
return true;
}
pending_count = 0;
return false;
}
private:
T buffer[Size];
std::atomic<size_t> head{0};
};
// Контейнер для очередей разных типов
struct SharedData {
CircularQueue<int, 100> int_queue;
CircularQueue<double, 100> double_queue;
std::mutex mtx;
std::condition_variable cv;
bool stop = false;
};
void writer(SharedData& data) {
for (int i = 1; i <= 5; ++i) {
{
std::lock_guard<std::mutex> lock(data.mtx);
data.int_queue.push(i * 10);
data.double_queue.push(i * 1.1);
std::cout << "[Writer] Pushed data set " << i << std::endl;
}
data.cv.notify_all(); // Активируем всех читателей
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
data.stop = true;
data.cv.notify_all();
}
void reader(int id, SharedData& data) {
size_t my_int_ptr = 0;
size_t my_dbl_ptr = 0;
while (true) {
std::unique_lock<std::mutex> lock(data.mtx);
data.cv.wait(lock, [&] { return data.stop || true; }); // Упрощенно: просыпаемся по сигналу
int val;
double dval;
size_t pending;
// Читаем из инт-очереди
while (data.int_queue.try_read(my_int_ptr, val, pending)) {
std::cout << "Reader " << id << " got INT: " << val << " (Left: " << pending << ")\n";
}
// Читаем из дабл-очереди
while (data.double_queue.try_read(my_dbl_ptr, dval, pending)) {
std::cout << "Reader " << id << " got DBL: " << dval << " (Left: " << pending << ")\n";
}
if (data.stop) break;
}
}
int main() {
SharedData data;
std::thread w(writer, std::ref(data));
std::thread r1(reader, 1, std::ref(data));
std::thread r2(reader, 2, std::ref(data));
w.join();
r1.join();
r2.join();
return 0;
}
// Основные моменты реализации (от ИИ):
// Индивидуальные указатели: Переменные my_int_ptr и my_dbl_ptr находятся в стеке каждого читающего потока. Они отслеживают прогресс конкретного
// читателя относительно общей «головы» (head) очереди.
// Шаблон очереди: Позволяет создавать экземпляры для int, double или любых других типов в одном контейнере SharedData.
// Синхронизация: std::condition_variable используется для мгновенной активации читателей пишущим потоком.
// Статус очереди: Метод try_read возвращает pending_count, что позволяет читателю знать, сколько данных еще осталось в буфере именно для него.
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
27.02.2026 13:34:14
Цитата
VPM написал: Да, спасибо за вариант, про собственный флаг, даже не задумывался, взял на вооружение!
Этот вариант наглядный, но не самый эффективный и гибкий. Сам я использую для управления, всего двумя нужными мне коллбеками: OnCleanUp и OnTransReply их переопределение, то есть присвоение переменным коллбеков тех функций, которые мне нужны для обработки соответствующих событий. Например, если я не хочу (по как то причине) обрабатывать события OnTransReply, то выполняется OnTransRe ply = function() end Для продолжения обработки значение переменной OnTransReply заменяется на нужную мне функцию обработки (запись в очередь параметров событий).
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
27.02.2026 11:25:03
Цитата
Йцукен написал: Это теоритическое предположение или есть исследования на эту тему?
Это не предположение. Вы это можете проверить эмпирически в своих скриптах. Это будет тем заметнее, чем больше скриптов работает одновременно и чем большая работа выполняется при инициализации скриптов.
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
27.02.2026 11:20:56
Цитата
VPM написал: То есть это полноценная модульная система. Так?
Да.
И еще существенное замечание:
Цитата
Nikolay написал: Приход колбека должен быть не прерыванием, а добавлением события в очередь. Когда завершите всю работу с инициализацией, то разберете колбеки что пришли, если пришли. А порядок инициализации не испортится, если это сделать не в OnInit.
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Выполняйте инициализацию в main (при отключенных коллбеках) и используйте, как написал Nikolay, вместо dofile, require.
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
27.02.2026 10:49:22
1.
Цитата
VPM написал: Ну хорошо, давайте по порядку. Вот вариант от TGB , Так?
Не так. Прочитайте мое сообщение #737.
2.
Цитата
VPM написал: Именно здесь, ни кто не по беспокоит, ни какие колбеки не прилетят, пока зависимости в модулях не установлены.
Тогда почему бы вам не сделать управление коллбеками, хотя бы так?:
Код
-- В самом начале вашего текста скрипта ---
is_Callback = false --- Коллбеки вас не беспокоят --
--- Во всех ваших коллбеках первая строка:
if not is_Callback then return end
--- В любом месте вашего скрипта.
-- Включить обработку коллбеков:
is_Callback = true
-- Выключить обработку коллбеков:
is_Callback = false
И пока вы в своем скрипте не выполните; is_Callback = true коллбеки вас не будут беспокоить. Если вам в скрипте, по какой то надобности, потребуется отключить обработку коллбеков, то: is_Callback = false.
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
27.02.2026 09:36:37
Цитата
VPM написал: TGB , Ну так огромная! А именно в локализации глобальных переменных для данного проекта. Да медленнее но Гарантированно!
А так быстро и гарантированно (с локализацией глобальных переменных для данного проекта):
Код
function OnInit_VPM()
-- Логирование (ваш код)
Logger.init(log_file)
Logger.log("INIT", "", "Starting robot")
---- Весь остальной ваш код ---
end
function main()
--- Инициализация (в начале main)--
OnInit_VPM()
--- Дальше ваш код ---
end
Зачем вам "медленно но Гарантированно!", когда можно быстро и гарантированно? Что из написанного мной вам непонятно? Более того, как я отмечал:
Цитата
TGB написал: в рассмотренном варианте при запуске нескольких скриптов сразу они начинают выполняться быстрее, чем с кодами в OnInit и body.
Это означает, что перезапуск QUIK, при инициализации в main, выполняется быстрее по сравнению с инициализацией в OnInit. Зачем вам долгий перезапуск? ------ Вы какой то Сусанин . Из возможных решений все время выбираете и рекламируете самые сложные и неэффективные. Вы хотите пользователей завести в болото
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
26.02.2026 21:12:39
Цитата
VPM написал: Ваши заключения абсолютно верны, и подход с использованием main для инициализации и работы в многозадачной среде действительно более эффективен в случае с QUIK, где важна производительность и отсутствие блокировок. Ну все этим пользуются, и я не исключение.
и А тогда зачем вы хотите использовать OnInit, выполняемый в служебном потоке? Где вы видите разницу между функцией OnInit, выполняемой перед запуском main и точно такой же функцией, но с другим именем, выполняемой сразу вначале main (кроме того что в первом случае загружается служебный поток)?
Функция getDepoEx может приводить к зависаниям терминала
Пользователь
Сообщений: Регистрация: 12.05.2020
26.02.2026 16:16:01
Цитата
Йцукен написал: потому что getDepoEx, getSecurityInfo, в случае ошибки не возвращают ничего (процедуры, другими словами).
В синтаксисе Lua есть понятия: результат функции (отсутствие/одно или несколько значений) и выражение. Функция может не возвращать результата, но есть синтаксические конструкции, которые рассматриваются как выражения (это можно посмотреть в описании языка). Например, присвоение: local v = f (). В этом случае вызов функции справа от равенства считается выражением. В выражении функция, не возвращающая результат, заменяется на nil. Результатом выражения может быть только одно значение. Явным способом привести к выражению можно синтаксическую конструкцию, заключив ее в круглые скобки. Например: message(type((getSecurityInfo("", "")))), результат печати: nil
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
26.02.2026 14:27:05
Цитата
TGB написал: можно разместить в начале main в виде функции
Уточнение: если очень хотите, определение этой функции в переменной с именем отличным от OnInit можно разместить в области body, но вызвать только в начале main.
Система принятия решений и/или Нечеткая логика(FuzzyLogic), Нечеткая логика или Система принятия решений в трейдинге
Пользователь
Сообщений: Регистрация: 12.05.2020
26.02.2026 14:01:52
Цитата
VPM написал: OnInit, или просто прозрение, которым хочется поделиться.
Я бы не стал это комментировать, но ведь кто то может это воспринять как открытие и руководство к действию. Попробую объяснить ваше заблуждение (очередное). Я в своих скриптах не использую ни OnInit ни кода в body по следующим причинам: 1) Код OnInit и код body будут исполняться в основном служебном единственном потоке, который обслуживает: - запуск всех Lua-скриптов пользователя; - запуск коллбеков всех Lua-скриптов пользователя; - обработка всех коллбеков таблиц QUIK (это не таблицы Lua); - обработка всех индикаторов пользователя. Это значит, что пока он будет "возиться" с кодами OnInit и body другого он ничего делать не будет. На всякий случай попробуйте в начале OnInit написать строку: sleep(30000) и посмотрите что получится . 2) Всю необходимую инициализацию (включая пакеты) можно разместить в начале main в виде функции, вызываемой сразу после ее определения и это будет выполняться в отдельном созданном потоке main, а основной служебный поток продолжит параллельно (если у ПК несколько ядер) выполнять свою перечисленную ранее работу. 3) в рассмотренном варианте при запуске нескольких скриптов сразу они начинают выполняться быстрее, чем с кодами в OnInit и body.
Функция getDepoEx может приводить к зависаниям терминала
Пользователь
Сообщений: Регистрация: 12.05.2020
26.02.2026 07:32:55
1.
Цитата
Йцукен написал: Предлагаю заинтересованным подтвердить или опровергнуть это.
Подтверждаю. В выложенном экземпляре QUIK у меня ошибка проявляется.
2.
Цитата
Nikolay написал: Да, любое зависание основного окна приложения - это ошибка, баг.
Согласен. Локализована типичная ошибка синхронизации. В моей песочнице эта ошибка не проявляется.
3.
Цитата
Йцукен написал: Но когда посреди торгов, у вас неожиданно всё зависнет, - прибежите на форум.
Согласен. Такие нетривиальные ошибки надо выкладывать и исправлять (разработчику), а пользователей, сумевших их локализовать, поощрять .
4. Вообще то, многие ошибки, обнаруженные в QUIK, по моему мнению, во многом, следствие существующей дефектной (сложной) его архитектуры , о чем я написал в своей ветке "Что стоило бы изменить в QUIK по-крупному" два года назад.
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
24.02.2026 15:45:40
Цитата
TGB написал: предложение можно сформулировать следующим образом: функциональность обработки событий QUIK расширить, добавив в API пользователя функцию задания свойств служебных очередей событий
Оформить это, наверное, лучше в виде меню задания свойств служебных очередей событий QLua, вызываемое в окне запуска скриптов пользователя, при отсутствии запущенных скриптов. Промежуточный итог обсуждения интерфейса QUIK со скриптами QLua, при реализации предложений этой ветки следующий: пользователям, при этом, вносить изменения в их существующие скрипты не требуется.
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
21.02.2026 12:49:30
У меня было предложение, которое стоит повторить:
Цитата
TGB написал: На форумах ARQA для комментирующих пользователей ввести месячный лимит трафика, после которого не будет возможность вводить комментарии. Значением этого лимита могло быть: Годовой трафик nikolz / 12 / 10. Но, конечно, насчет значения лимита, решать ARQA. Наличие такого лимита, обеспечило бы: - экономию дискового пространства баз форумов; - автоматическое модерирование форумов за счет принуждения думать о краткости и четкости текстов, пишущих комментаторами; - - удобство для читающих комментарии, в которых будет меньше флуда.
Это может показаться неактуальным из-за снижения активности пишущих, но могло бы улучшить качество форумов в перспективе.
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
20.02.2026 17:57:57
Цитата
Йцукен написал: Логичней было бы, чтобы терминал не дублировал пропущенные колбэки, по которым скрипт не получит новых данных.
Это хорошее ваше предложение можно сформулировать следующим образом: функциональность обработки событий QUIK расширить, добавив в API пользователя функцию задания свойств служебных очередей событий: 1) длин очередей; 2) функций фильтрации событий перед их записью в соответствующие очереди; 3) приоритетов обработки очередей; 4) может быть что то еще.. Все свойства имеют значение по умолчанию.
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
20.02.2026 14:48:52
Цитата
nikolz написал: Я специально выложил для начинающих писателей не псевдокод, а скрипт на Lua с очередью. Да и в документации на QLua есть еще один пример.
Какой же вы непонятливый. До сих пор не поняли, что обсуждаются не существующие реализации, а вариант изменения этих реализаций. Все таки, похоже, что вы полуинтеллектуальный робот-спамер устаревшей версии, без способности учета контекста обсуждаемой темы при генерации спама.
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
20.02.2026 14:21:42
Цитата
nikolz написал: все что делается внутри sleep - это передается ядру время на которое надо остановить поток.
Вы, как дятел , про свое, не относящееся к обсуждаемому. Вы это читали ?:
Цитата
TGB написал: -- Отличие только в реализации функции sleep. -- При ее вызове с заданным интервалом внутри нее выполняется (код на C++): WaitForSingleObject( , ); // здесь ожидание истечения или выдачи сигнала QUIK (неблокирующей функцией Pulse в служебном потоке) о записи в очереди новых событий. -- Когда sleep "срывается" с , то в ней анализируются очереди (это можно сделать эффективно, используя битовую шкалу непустых очередей скрипта) и выполняются соответствующие с параметрами считанными из очередей. -- Функционально это не отличается от того, что реализовано сейчас, но выполняется в потоке пользователя main.
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
20.02.2026 13:00:33
Цитата
nikolz написал: Sleep останавливает поток main, а функции колбеков вызываются в другом потоке, на который sleep не действует.
Вы опять только пишите не читая. Мы же с Йцукен обсуждаем не системный Sleep и даже sleep QLua, а предложенный мной запускающий коллбеки. Вам опять надо проложиться между комментариев ?
Если бы я был архитектором QUIK, Что стоило бы изменить в QUIK по-крупному
Пользователь
Сообщений: Регистрация: 12.05.2020
20.02.2026 12:35:25
Цитата
Йцукен написал: А как вы узнаете, что это повторы, пока не обработаете их?
Вы сначала создаете таблицу фильтрации со значениями полей фильтрации заведомо не совпадающими с ожидаемыми данными.