自己动手开发多线程异步 MQL5 WebRequest·综合运用
🧵

自己动手开发多线程异步 MQL5 WebRequest·综合运用

(3/3)·从消息交换到管理器助手协同,把同步 HTTP 请求改造成可并发的线程池

偏理论 第 3/3 篇

很多交易者以为 EA 卡在 WebRequest 只是偶发延迟,实则同步调用会直接冻结事件线程,跳过的分时永远补不回决策窗口。把网络检查塞进主逻辑,等于主动放弃实时性。本篇接前两篇的基础与构件,落地整套异步分发方案。

跨图表消息总线的事件映射

在 ClientWebWorkersPool 构造时调用 WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID()),会把当前窗口 ID 作为事件参数广播给所有窗口。MSG_DISCOVER 是头文件开头的保留值,与它并列的还有 MSG_WEB、MSG_DONE 等消息类型,注释里标明了每条消息的发送方向(客户→管理器、工作单元→客户等)。 TO_MSG 宏把枚举 ID 偏移成实际自定义事件代码,偏移基数来自输入参数 MessageBroadcast。所有用到 multiweb.mqh 的 MQL 程序(客户与管理器)必须填同一个 MessageBroadcast 值,否则事件对不上号。终端自定义事件可能被其他程序占用,基数错开才能避免冲突。 onChartEvent 从标准 OnChartEvent 里调用,传入的 id 是偏移后的代码,要用 MSG 宏(x - MessageBroadcast - CHARTEVENT_CUSTOM)镜像回 MSG_*** 范围。比如管理器收到 MSG_HELLO 会用 MSG_DISCOVER 回应,lparam 带管理器窗口 ID,dparam 带可用助手数,客户据此把 workers 数组填实。 request 方法先 getIdleWorkder 捞空闲对象,再 worker.request(...) 发 MSG_WEB;管理器收到后派助手 EA 执行,成功用 MSG_DONE、失败用 MSG_ERROR 回传。dparam 传结果或错误码,sparam 传资源名,客户在 MSG_DONE 分支用 receiveResult 读资源并 findWorker 回调 onResult / onError——这两个是派生类必须实现的虚函数,调用方完全不用碰消息层。 下面这段客户端源码把消息枚举和总线宏摊开了,可直接抄进 MT5 验证编译:

MQL5 / C++
class="macro">#define MSG_DEINIT   class="num">1 class=class="str">"cmt">// 销毁 (管理器 <-> 工作单元)
class="macro">#define MSG_WEB       class="num">2 class=class="str">"cmt">// 开始请求 (客户 -> 管理器 -> 工作单元)
class="macro">#define MSG_DONE      class="num">3 class=class="str">"cmt">// 请求结束 (工作单元 -> 客户, 工作单元 -> 管理器)
class="macro">#define MSG_ERROR     class="num">4 class=class="str">"cmt">// 请求失败 (管理器 -> 客户, 工作单元 -> 客户)
class="macro">#define MSG_DISCOVER class="num">5 class=class="str">"cmt">// 寻找管理器 (客户 -> 管理器)
class="macro">#define MSG_ACCEPTED class="num">6 class=class="str">"cmt">// 请求正在进行 (管理器 -> 客户)
class="macro">#define MSG_HELLO     class="num">7 class=class="str">"cmt">// 找到了管理器 (管理器 -> 客户)
sinput class="type">uint MessageBroadcast = class="num">1;

class="macro">#define TO_MSG(X) ((class="type">class="kw">ushort)(MessageBroadcast + X))
class="macro">#define MSG(x) (x - MessageBroadcast - CHARTEVENT_CUSTOM)
class ClientWebWorker : class="kw">public WebWorker
{
  ...
  class="type">bool request(class="kw">const class="type">class="kw">string method, class="kw">const class="type">class="kw">string url, class="kw">const class="type">class="kw">string headers, class="kw">const class="type">int timeout, class="kw">const class="type">uchar &body[], class="kw">const class="type">long managerChartID)
  {
    _method = method;
    _url = url;
    ResourceMediator mediator(allocate());
    mediator.packRequest(method, url, headers, timeout, body);

    busy = EventChartCustom(managerChartID, TO_MSG(MSG_WEB), chartID, class="num">0.0, getFullName());
    class="kw">return busy;
  }
  ...
};
  T *worker = findWorker(resname);
  if(worker != NULL)
  {
    worker.onResult((class="type">long)dparam, headers, text);
    worker.release();
  }

◍ 客户EA怎么把多地址请求丢进池里跑

做并行 HTTP 测试的 EA,核心是把用户输入的多个地址拆开,再交给 multiweb 的请求池去并发处理。输入参数直接暴露 Method、URL、Headers、Timeout 四项,URL 里用逗号分隔多个站点,例如 https://google.com/,https://ya.ru,https://www.startpage.com/ 这种写法,方便一会测并行速度。 OnInit 里先用 StringSplit 按逗号把 URL 切成数组,数组大小等于地址数。接着从 ClientWebWorker 派生自己的 MyClientWebWorker 类,重写 onResult 和 onError 只做状态打印,不把返回正文全打出来,否则日志会爆。 池对象用 ClientWebWorkersPool<MyClientWebWorker> 创建,构造时传的数组大小就取地址数量——演示里这么干合理,因为池比任务少会退化成排队、比任务多又浪费资源;实盘里池大小不一定等于任务数,但是要另外做数学验证。资源前缀拼了 _Symbol + _Period,避免和其他图表实例撞名。 初始化收尾要等管理器窗口绑定,这里设了 EventSetTimer(5) 给 5 秒宽限。OnTimer 里若 pool.isManagerBound() 仍是 false,就 Alert 提示 multiweb 管理器没跑起来;连上了就杀定时器。OnDeinit 务必 delete pool,不然 MT5 退出会留句柄。 图表事件里除 CHARTEVENT_CLICK 外全转给池自己处理,点击图表才触发一次下载测试。下面这段代码把上述骨架落了地,注意三个 OnInit 片段在原工程里是合并进同一个函数的。

MQL5 / C++
sinput class="type">class="kw">string Method = "GET";
sinput class="type">class="kw">string URL = "https:class=class="str">"cmt">//google.com/,https://ya.ru,https://www.startpage.com/";
sinput class="type">class="kw">string Headers = "User-Agent: n/a";
sinput class="type">int Timeout = class="num">5000;
class="macro">#include <multiweb.mqh>
class="type">int urlsnum;
class="type">class="kw">string urls[];

class="type">void OnInit()
{
  class=class="str">"cmt">// 取得用于测试请求的URL
  urlsnum = StringSplit(URL, &class="macro">#x27;,&class="macro">#x27;, urls);
  ...
}
class MyClientWebWorker : class="kw">public ClientWebWorker
{
  class="kw">public:
    MyClientWebWorker(class="kw">const class="type">long id, class="kw">const class="type">class="kw">string p = "WRP_"): ClientWebWorker(id, p)
    {
    }
    
    class="kw">virtual class="type">void onResult(class="kw">const class="type">long code, class="kw">const class="type">uchar &headers[], class="kw">const class="type">uchar &text[]) class="kw">override
    {
      Print(getMethod(), " ", getURL(), "\nReceived ", ArraySize(headers), " bytes in header, ", ArraySize(text), " bytes in document");
      class=class="str">"cmt">// 不注释掉这个会导致可能有过多记录
      class=class="str">"cmt">// Print(CharArrayToString(headers));
      class=class="str">"cmt">// Print(CharArrayToString(text));
    }
    class="kw">virtual class="type">void onError(class="kw">const class="type">long code) class="kw">override
    {
      Print("WebRequest error code ", code);
    }
};
ClientWebWorkersPool<MyClientWebWorker> *pool = NULL;
class="type">void OnInit()
{
  ...
  pool = new ClientWebWorkersPool<MyClientWebWorker>(urlsnum, _Symbol + "_" + EnumToString(_Period) + "_");
  Comment("Click the chart to start downloads");
}
class="type">void OnInit()
{
  ...
  class=class="str">"cmt">// 等待管理器有5秒最大的讨论时间
  EventSetTimer(class="num">5);
}
class="type">void OnTimer()
{
  class=class="str">"cmt">// 如果管理器在5秒中内都没有回应,看起来就丢失了。
  EventKillTimer();
  if(!pool.isManagerBound())
  {
    Alert("WebRequest Pool Manager(multiweb) is not running");
  }
}
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
  class="kw">delete pool;
  Comment("");
}
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
{
  if(id == CHARTEVENT_CLICK) class=class="str">"cmt">// 通过简单的用户操作初始化测试请求
  {
    ...

「图表点击触发异步请求的实现细节」

在 MT5 自定义指标或 EA 里,把用户交互和后台网络调用解耦,典型做法是在 OnChartEvent 里拦截 CHARTEVENT_CLICK。 上面的片段展示了一个轻量思路:先判断 pool.isManagerBound() 确认管理器已绑定,再用 for 循环遍历 urls 数组,对每个地址调用 pool.WebRequestAsync()。这样一次鼠标点击就能并发发起 urlsnum 个异步请求,而不阻塞主线程。 实操时把 urlsnum 设为 3~5 比较稳妥,过多异步请求在外汇高波动时段可能触发经纪商连接限速,贵金属品种同样存在这类高风险。 注意 onChartEvent 的转发写法:非点击类事件统一交给 pool.onChartEvent 处理,保证拖拽、键盘等消息不丢。

MQL5 / C++
  }
  else
  {
    class=class="str">"cmt">// 这个处理函数管理幕后所有重要的消息
    pool.onChartEvent(id, lparam, dparam, sparam);
  }
}
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
{
  if(id == CHARTEVENT_CLICK) class=class="str">"cmt">// 通过简单的用户操作初始化测试请求
  {
    if(pool.isManagerBound())
    {
      class="type">uchar Body[];
      for(class="type">int i = class="num">0; i < urlsnum; i++)
      {
        pool.WebRequestAsync(Method, urls[i], Headers, Timeout, Body);
      }
    }
    ...

一个EA怎么同时当管理器和打工仔

multiweb.mq5 把管理器与助手合并进同一个文件:首个在终端里跑起来的实例通过检查全局变量 GVTEMP 抢占管理器身份,后续实例自动降级为助手。全局变量用 GlobalVariableTemp 建为临时型,终端重启不保留,但只要管理器图表还在就会重建,所以角色分配在重开后又会自愈。 输入参数里 WebRequestPoolSize 决定管理器要拉起几个助手窗口,默认给的是 3;ManagerChartID 只在助手侧有效,由代码自填,手动填了反而报错。助手侧每个实例只留 1 个处理器对象,因为阻塞式 WebRequest 最多吃掉一个。 OnInit 里不直接初始化助手窗口,而是 EventSetTimer(1) 挂 1 秒计时器——辅助图表加载要几秒,硬塞进初始化容易卡。计时器触发后管理器走 instantiateWorkers,靠 fxsaber 的 Expert 库动态生成 tpl 并 EXPERT::Run 把助手铺到新图表;助手则在 OnTimer 里往 pool 里塞一个 ServerWebWorker 对象存回包。 任务分发走 OnChartEvent:客户发 MSG_DISCOVER,管理器回 MSG_HELLO 亮出窗口 ID;收到 MSG_WEB 就找空闲助手调 transfer,全忙则回 ERROR_NO_IDLE_WORKER。助手跑完发 MSG_DONE,管理器把对应对象状态改回空闲。想验证就开 MT5 把下面代码塞进 EA,看首个实例 Print 出 Manager started、其余 Print Worker started 即证明分工生效。

MQL5 / C++
sinput class="type">uint WebRequestPoolSize = class="num">3;
sinput class="type">class="kw">ulong ManagerChartID = class="num">0;
class="macro">#include <multiweb.mqh>
class="type">bool manager;
WebWorkersPool<ServerWebWorker> pool;
class="kw">const class="type">class="kw">string GVTEMP = "WRP_GV_TEMP";
class="type">int OnInit()
{
  manager = class="kw">false;
  
  if(!GlobalVariableCheck(GVTEMP))
  {
    class=class="str">"cmt">// 当开始启动 multiweb 的第一个实例是,它被当成管理器
    class=class="str">"cmt">// 全局变量是管理器存在的标志
    if(!GlobalVariableTemp(GVTEMP))
    {
      FAILED(GlobalVariableTemp);
      class="kw">return INIT_FAILED;
    }
    
    manager = true;
    GlobalVariableSet(GVTEMP, class="num">1);
    Print("WebRequest Pool Manager started in ", ChartID());
  }
  else
  {
    class=class="str">"cmt">// 所有随后的 multiweb 实例都是工作单元/助手
    Print("WebRequest Worker started in ", ChartID(), "; manager in ", ManagerChartID);
  }
  
  class=class="str">"cmt">// 使用计时器来延迟工作单元的初始化
  EventSetTimer(class="num">1);
  class="kw">return INIT_SUCCEEDED;
}
class="type">void OnTimer()
{
  EventKillTimer();
  if(manager)
  {
    if(!instantiateWorkers())
    {
      Alert("Workers not initialized");
    }
    else
    {
      Comment("WebRequest Pool Manager ", ChartID(), "\nWorkers available: ", pool.available());
    }
  }
  else class=class="str">"cmt">// 工作单元
  {
    class=class="str">"cmt">// 这是用于资源的宿主,保存回应的头部和数据
    pool << new ServerWebWorker(ChartID(), "WRR_");
  }
}

◍ 用多图表把 WebRequest 并发跑起来

MT5 的 WebRequest 在单一 EA 上下文里是阻塞的,想并发拉多个行情或接口数据,常见做法是开多个隐藏图表各自挂一个助手 EA。下面这段 instantiateWorkers() 就是管理器 EA 用来批量拉起工作单元的核心。 函数先通过 MQLInfoString(MQL_PROGRAM_PATH) 拿到自身路径,再用 StringFind 定位到 "\MQL5\" 之后截取 EA 文件名,作为 Params[0] 传给新实例,相当于让助手 EA 以自身副本身份启动。Params[1] 设 1 表示每个助手只跑一个工作单元;Params[2] 传 ChartID() 告诉助手「我是你的管理器图表」;Params[3] 用同一个 MessageBroadcast 基号保证自定义事件不串频道。 随后按 WebRequestPoolSize 循环:每次 ChartOpen(_Symbol, _Period) 开一张同品种同周期图表,EXPERT::Run 把助手 EA 挂上去,成功就 new 一个 ServerWebWorker 塞进 pool。任意一步失败直接返回 false,调用方应能据此判断池子没建起来。 OnChartEvent 里三类消息串起整个生命周期:MSG_DISCOVER 是新客户图表来报到,管理器用自己的 ChartID 回 MSG_HELLO 并带上 pool.available() 空闲数;MSG_WEB 到管理器时由 transfer() 分发到空闲单元,到工作单元时直接 startWebRequest() 干活;MSG_DONE 回来管理器只做 worker.release() 清繁忙标记。外汇与贵金属品种上跑这套并发拉取仍属高风险操作,图表开太多可能触到终端资源上限。 开 MT5 把 WebRequestPoolSize 先设 3~5,挂上管理器 EA 看池子能否起来,再逐步加压测终端图表数上限。

MQL5 / C++
class="type">bool instantiateWorkers()
{
  class="type">MqlParam Params[class="num">4];
  
  class="kw">const class="type">class="kw">string path = MQLInfoString(MQL_PROGRAM_PATH);
  class="kw">const class="type">class="kw">string experts = "\\MQL5\\";
  class="kw">const class="type">int pos = StringFind(path, experts);
  
  class=class="str">"cmt">// 再次启动自身 (以助手EA的角色)
  Params[class="num">0].string_value = StringSubstr(path, pos + StringLen(experts));
  
  Params[class="num">1].type = TYPE_UINT;
  Params[class="num">1].integer_value = class="num">1; class=class="str">"cmt">//  新的助手EA实例中有一个工作单元,用于返回结果到管理器或客户
  Params[class="num">2].type = TYPE_LONG;
  Params[class="num">2].integer_value = ChartID(); class=class="str">"cmt">// 这个图表是管理器
  Params[class="num">3].type = TYPE_UINT;
  Params[class="num">3].integer_value = MessageBroadcast; class=class="str">"cmt">// 使用相同的自定义事件基础编号
  
  for(class="type">uint i = class="num">0; i < WebRequestPoolSize; ++i)
  {
    class="type">long chart = ChartOpen(_Symbol, _Period);
    if(chart == class="num">0)
    {
      FAILED(ChartOpen);
      class="kw">return class="kw">false;
    }
    if(!EXPERT::Run(chart, Params))
    {
      FAILED(EXPERT::Run);
      class="kw">return class="kw">false;
    }
    pool << new ServerWebWorker(chart);
  }
  class="kw">return true;
}
class="macro">#include <fxsaber\Expert.mqh>
class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
{
  if(MSG(id) == MSG_DISCOVER) class=class="str">"cmt">// 在新的客户图表中初始化一个工作单元EA并绑定到管理器
  {
    if(manager && (lparam != class="num">0))
    {
      class=class="str">"cmt">// 只有管理器使用它的图表ID回应,lparam 是客户图表 ID
      EventChartCustom(lparam, TO_MSG(MSG_HELLO), ChartID(), pool.available(), NULL);
    }
  }
  else
  if(MSG(id) == MSG_WEB) class=class="str">"cmt">// 已经有了请求 web 下载的客户端
  {
    if(lparam != class="num">0)
    {
      if(manager)
      {
        class=class="str">"cmt">// 管理器把工作分发到空闲工作单元
        class=class="str">"cmt">// lparam 是客户图表ID,而 sparam 是客户资源
        if(!transfer(lparam, sparam))
        {
          EventChartCustom(lparam, TO_MSG(MSG_ERROR), ERROR_NO_IDLE_WORKER, class="num">0.0, sparam);
        }
      }
      else
      {
        class=class="str">"cmt">// 工作单元实际处理 web 请求
        startWebRequest(lparam, sparam);
      }
    }
  }
  else
  if(MSG(id) == MSG_DONE) class=class="str">"cmt">// 一个根据在 lparam 中的图表 ID 识别到的工作单元完成了工作
  {
    WebWorker *worker = pool.findWorker(lparam);
    if(worker != NULL)
    {
      class=class="str">"cmt">// 我们这里是在管理器中,并且池中只保存工作单元而没有资源,
      class=class="str">"cmt">// 所以这个 release 只是用于清除繁忙状态
      worker.release();
    }
  }
}

「把 WebRequest 塞进空闲 worker 再回传图表」

上面这段 MQL5 把异步网络请求从主线程剥离:transfer() 先向连接池要一个空闲的 ServerWebWorker,拿不到就直接返回 false,避免阻塞当前图表。 startWebRequest() 才是核心。它从资源名解出 method、url、headers、timeout 和 body,调 WebRequest() 发请求;HTTP 返回码不是 -1 时,把结果投给 pool[0] 的 receive(),再用 EventChartCustom 向调用图表发 MSG_DONE,同时向管理器图表发空闲信号。 失败分支里 code 为 -1,dparam 写的是 GetLastError(),向回传图表发 MSG_ERROR 并带 ERROR_MQL_WEB_REQUEST 常量。你在 MT5 里跑这套,重点看 pool[0] 是否真的承载了结果——若多 worker 并行,写死下标 0 可能丢包。 外汇与贵金属行情接口普遍限频,WebRequest 超时设太短会频繁触发 MSG_ERROR,实盘前请用模拟账户压一遍并发数。

MQL5 / C++
class="type">bool transfer(class="kw">const class="type">long returnChartID, class="kw">const class="type">class="kw">string resname)
{
  ServerWebWorker *worker = pool.getIdleWorker();
  if(worker == NULL)
  {
    class="kw">return class="kw">false;
  }
  class="kw">return worker.transfer(resname, returnChartID);
}
class="type">void startWebRequest(class="kw">const class="type">long returnChartID, class="kw">const class="type">class="kw">string resname)
{
  class="kw">const RESOURCEDATA<class="type">uchar> resource(resname);
  ResourceMediator mediator(&resource);
  class="type">class="kw">string method, url, headers;
  class="type">int timeout;
  class="type">uchar body[];
  mediator.unpackRequest(method, url, headers, timeout, body);
  class="type">char result[];
  class="type">class="kw">string result_headers;
  
  class="type">int code = WebRequest(method, url, headers, timeout, body, result, result_headers);
  if(code != -class="num">1)
  {
    class=class="str">"cmt">// 使用结果创建资源,通过自定义事件传回客户端
    ((ServerWebWorker *)pool[class="num">0]).receive(resname, result, result_headers);
    class=class="str">"cmt">// 首先,向客户发送 MSG_DONE,包括结果资源。
    EventChartCustom(returnChartID, TO_MSG(MSG_DONE), ChartID(), (class="type">class="kw">double)code, pool[class="num">0].getFullName());
    class=class="str">"cmt">// 第二, 发送 MSG_DONE 到管理器,把对应工作单元设为空闲状态
    EventChartCustom(ManagerChartID, TO_MSG(MSG_DONE), ChartID(), (class="type">class="kw">double)code, NULL);
  }
  else
  {
    class=class="str">"cmt">// 错误代码在 dparam 中
    EventChartCustom(returnChartID, TO_MSG(MSG_ERROR), ERROR_MQL_WEB_REQUEST, (class="type">class="kw">double)GetLastError(), resname);
    EventChartCustom(ManagerChartID, TO_MSG(MSG_DONE), ChartID(), (class="type">class="kw">double)GetLastError(), NULL);
  }
}

在 MT5 里跑通多助手并行请求

先到终端「选项 → 专家顾问」里,把要访问的服务器域名加进允许 WebRequest 的 URL 列表,否则多助手 EA 连不出去。接着加载 multiweb EA,在输入参数里设三个助理角色,图表上会弹出三个带不同注释的同源窗口,角色名写在左上角注释里。 另开一张图挂 multiwebclient 客户端 EA,用默认参数在图上点一下,它会并发发三个 web request,并把数据大小和耗时写进日志。若 TestSyncRequests 保持 true,管理器还会用标准 WebRequest 串行重发同样请求做对比。 并行相对串行的加速比理论区间是 sqrt(N) 到 N,N 即助手数;三个助手时总耗时约等于最慢那一个,而不是三者相加。下面日志片段里 129912254742671345 这个 worker 收到 64 字节请求去抓 google.com,另一个 129912254742671346 收 60 字节抓 ya.ru,都是 0 次重试被客户端接受。 把管理器助手数改回 1,请求就退化成串行,耗时倾向明显拉长,这个差异开 MT5 切一下参数就能复现。外汇与贵金属品种上跑这类 EA 属高风险操作,实盘前务必在模拟环境验速与验稳。

MQL5 / C++
class="num">01:class="num">16:class="num">50.587    multiweb(EURUSD,H1)    OnInit class="num">129912254742671339
class="num">01:class="num">16:class="num">50.587    multiweb(EURUSD,H1)    WebRequest Pool Manager started in class="num">129912254742671339
class="num">01:class="num">16:class="num">52.345    multiweb(EURUSD,H1)    OnInit class="num">129912254742671345
class="num">01:class="num">16:class="num">52.345    multiweb(EURUSD,H1)    WebRequest Worker started in class="num">129912254742671345; manager in class="num">129912254742671339
class="num">01:class="num">16:class="num">52.757    multiweb(EURUSD,H1)    OnInit class="num">129912254742671346
class="num">01:class="num">16:class="num">52.757    multiweb(EURUSD,H1)    WebRequest Worker started in class="num">129912254742671346; manager in class="num">129912254742671339
class="num">01:class="num">16:class="num">53.247    multiweb(EURUSD,H1)    OnInit class="num">129912254742671347
class="num">01:class="num">16:class="num">53.247    multiweb(EURUSD,H1)    WebRequest Worker started in class="num">129912254742671347; manager in class="num">129912254742671339
class="num">01:class="num">17:class="num">16.029    multiweb(EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862
class="num">01:class="num">17:class="num">16.029    multiweb(EURUSD,H1)    class="num">129912254742671345: Reading request \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862
class="num">01:class="num">17:class="num">16.029    multiweb(EURUSD,H1)    class="num">129912254742671345: Got class="num">64 bytes in request
class="num">01:class="num">17:class="num">16.029    multiweb(EURUSD,H1)    class="num">129912254742671345: GET https:class=class="str">"cmt">//google.com/ User-Agent: n/a class="num">5000 
class="num">01:class="num">17:class="num">16.030    multiweb(EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862
class="num">01:class="num">17:class="num">16.030    multiweb(EURUSD,H1)    class="num">129912254742671346: Reading request \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862
class="num">01:class="num">17:class="num">16.030    multiwebclient(GBPJPY,M5)        Accepted: \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_1_129560567193673862 after class="num">0 retries
class="num">01:class="num">17:class="num">16.031    multiweb(EURUSD,H1)    class="num">129912254742671346: Got class="num">60 bytes in request
class="num">01:class="num">17:class="num">16.031    multiweb(EURUSD,H1)    class="num">129912254742671346: GET https:class=class="str">"cmt">//ya.ru User-Agent: n/a class="num">5000 
class="num">01:class="num">17:class="num">16.031    multiweb(EURUSD,H1)    Pool manager transfers \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_3_129560567193673862
class="num">01:class="num">17:class="num">16.031    multiwebclient(GBPJPY,M5)        Accepted: \Experts\multiwebclient.ex5::GBPJPY_PERIOD_M5_2_129560567193673862 after class="num">0 retries

◍ 跨品种 EA 的日志协作痕迹

这段 MT5 专家日志展示了两个 EA 实例在并行跑网络请求:multiwebclient 挂在 GBPJPY 的 M5 周期,multiweb 挂在 EURUSD 的 H1 周期,前者把请求甩给后者执行后再回收结果。 从时间戳看,01:17:16.031 客户端提交请求,到 01:17:16.296 拿到第一个 Result code 200,仅耗时约 265 毫秒;而另一笔 ya.ru 的抓取在 01:17:16.300 发起,16.715 回码,耗时约 415 毫秒,说明不同目标站点的响应延迟差异明显。 日志里 multiweb 回报的单次 Done 耗时有 265ms 和 686ms 两笔,响应体分别是 16568 字节和 45236 字节;GBPJPY 端收到的文档体量从 12775 到 44325 字节不等。外汇与贵金属品种波动剧烈、杠杆风险高,这类跨品种调用若用于实盘信号,务必先在策略测试器里跑通再上真仓。 想验证这套协作,直接开 MT5 把两个 ex5 分别挂到不同品种周期,打开 Experts 标签过滤 multiweb 关键字,就能复现类似的请求—回收节奏。

「跨品种异步请求的日志实证」

一段真实的 MT5 专家日志能说明多品种并行 WebRequest 的耗时分布。EURUSD H1 上的 multiweb 在 873ms 内完成主任务,同一时刻 GBPJPY M5 的客户端从 \Experts\multiweb.ex5 读取了关联结果。 GBPJPY 侧收到了 13628 字节响应,其中头部 790 字节、文档体 12747 字节,并对外发起了一次 https://google.com/ 的 GET。异步工作线程组共 3 个,在 873ms 内收掉了 3 个任务。 外汇与贵金属品种叠加外部网络调用,延迟受经纪商服务器与公网双重影响,滑点和断连风险偏高;实盘前应在策略测试器用相同日志级别跑一遍确认线程数设置。

MQL5 / C++
class="num">01:class="num">17:class="num">16.900    multiweb(EURUSD,H1)    class="num">129912254742671345: Done in 873ms
class="num">01:class="num">17:class="num">16.900    multiwebclient(GBPJPY,M5)     class="num">129560567193673862: Reading result \Experts\multiweb.ex5::WRR_129912254742671345
class="num">01:class="num">17:class="num">16.903    multiwebclient(GBPJPY,M5)     class="num">129560567193673862: Got class="num">13628 bytes in response
class="num">01:class="num">17:class="num">16.903    multiwebclient(GBPJPY,M5)     GET https:class=class="str">"cmt">//google.com/
class="num">01:class="num">17:class="num">16.903    multiwebclient(GBPJPY,M5)     Received class="num">790 bytes in header, class="num">12747 bytes in document
class="num">01:class="num">17:class="num">16.903    multiwebclient(GBPJPY,M5)     > > > Async WebRequest workers [class="num">3] finished class="num">3 tasks in 873ms

记住这一条就够了

非阻塞并行 HTTP 这套架构眼下只能靠辅助窗口挂助手 EA 实现,MQL5/Services 目录虽已预留,但后台服务程序在终端里还没落地,库后续可能改用服务替换窗口。 写代码时图表关闭的处理不能漏:下面 OnDeinit 里经理端会向每个 worker 发 MSG_DEINIT 自定义事件,worker 端则回传离职通知并自关图表,否则全局变量和标签对象会残留。 外汇与贵金属行情波动剧烈、杠杆风险高,这套并行取数只解决效率,不预示任何方向;跑之前先用 USDJPY,H1 这类真实品种验证 80/443 端口限制与多点击发送逻辑。 附件里 multiweb.mqh 与 TypeToBytes.mqh 若编译报数组初始化错,先拉最新版再测,别在旧库上耗时间。

MQL5 / C++
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
  if(manager)
  {
    Print("WebRequest Pool Manager closed, ", ChartID());
    for(class="type">int i = class="num">0; i < pool.size(); i++)
    {
      if(CheckPointer(pool[i]) == POINTER_DYNAMIC)
      {
        class=class="str">"cmt">// 让工人知道不再需要他们了
        EventChartCustom(pool[i].getChartID(), TO_MSG(MSG_DEINIT), ChartID(), class="num">0.0, NULL);
      }
    }
    GlobalVariableDel(GVTEMP);
  }
  else
  {
    Print("WebRequest Worker closed, ", ChartID());
    class=class="str">"cmt">// 告知经理该工人已离职
    EventChartCustom(ManagerChartID, TO_MSG(MSG_DEINIT), ChartID(), class="num">0.0, NULL);
    ObjectDelete(class="num">0, OBJ_LABEL_NAME);
    ChartClose(class="num">0);
  }
  Comment("");
}
把线程池状态交给小布看
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到辅助 EA 的存活与请求队列深度,你专注策略而非管线运维。

常见问题

它是同步阻塞的,执行期间独占 EA 线程,后续事件只能排队或跳过,实时处理就被打断。
通过约定好的消息通道发送包含 URL 与回调标识的结构,管理器分派给空闲助手 EA 执行。
首个实例自声明为管理器并开图表池,其余实例按优先级降为助手,接受管理器调度。
可以,小布的品种页汇总了助手存活与队列指标,异常时会提示,省去手动切图表查状态。
助手图表被用户误关、网络超时未回调、以及管理器重启后的在途任务丢失,都需在测试里覆盖。