自己动手开发多线程异步 MQL5 WebRequest·进阶篇
🧵

自己动手开发多线程异步 MQL5 WebRequest·进阶篇

(2/3)· 当 EA 卡在 HTTP 请求上错过分时,三层线程架构能把延迟藏进辅助图表

含代码示例偏理论 第 2/3 篇
把 WebRequest 直接写进 OnTick 的 EA,遇到慢接口就会冻结整个事件队列,新分时被跳过只是时间问题。很多人误以为加个定时器就能解决,其实控制权根本没交还内核。外汇贵金属行情瞬息,这种隐性卡顿可能让你错失关键入场窗口。

把 HTTP 请求塞进资源字节流

在 MT5 里做跨图表传递或离线回放时,直接丢字符串和数组容易乱序,更稳的做法是把请求各字段按字节拼进一个 uchar 数组再存成 resource。下面这段打包逻辑先记好元数据的各段长度:t 是 int 的字节数(通常 4),b 是 body 的 ArraySize 返回值。 metadata.lengths.t = sizeof(int); metadata.lengths.b = ArraySize(body); // 分配结果数组,用于元数据和参数数据 uchar data[]; ArrayResize(data, LEADSIZE + metadata.total()); // 把元数据以字节数组方式放在数组的开始 ArrayCopy(data, metadata.sizes); // 把数据栏位挨个放到数组中 int cursor = LEADSIZE; uchar temp[]; StringToCharArray(method, temp); ArrayCopy(data, temp, cursor); ArrayResize(temp, 0); cursor += metadata.lengths.m; StringToCharArray(url, temp); ArrayCopy(data, temp, cursor); ArrayResize(temp, 0); cursor += metadata.lengths.u; StringToCharArray(headers, temp); ArrayCopy(data, temp, cursor); ArrayResize(temp, 0); cursor += metadata.lengths.h; int2chars.x = timeout; ArrayCopy(data, int2chars.b, cursor); cursor += metadata.lengths.t; ArrayCopy(data, body, cursor); // 在资源中存储数组 resource = data; 解包时反向读即可,重点是用 resource.Get(array) 把字节拉回来,再按 LEADSIZE 之后的 cursor 偏移逐段 CharArrayToString。实测打印出的 n 就是整个请求体的字节数,比如一次带 200 字节 body 的 POST 可能得到 200+LEADSIZE+各字符串长度。 void unpackRequest(string &method, string &url, string &headers, int &timeout, uchar &body[]) { uchar array[]; // 使用资源中的数据填充数组 int n = resource.Get(array); Print(ChartID(), ": Got ", n, " bytes in request"); // 从数组中读取元数据 ArrayCopy(metadata.sizes, array, 0, 0, LEADSIZE); int cursor = LEADSIZE; // 一个个读取所有的数据栏位 method = CharArrayToString(array, cursor, metadata.lengths.m); cursor += metadata.lengths.m; url = CharArrayToString(array, cursor, metadata.lengths.u); cursor += metadata.lengths.u; headers = CharArrayToString(array, cursor, metadata.lengths.h); cursor += metadata.lengths.h; ArrayCopy(int2chars.b, array, 0, cursor, metadata.lengths.t); timeout = int2chars.x; cursor += metadata.lengths.t; if(metadata.lengths.b > 0) { ArrayCopy(body, array, 0, cursor, metadata.lengths.b); } } 响应侧另有 packResponse(const string source, const uchar &result[], const string &result_headers),结构同理,只是字段换成来源标识和返回头。外汇与贵金属行情接口调用属高风险操作,字节错位可能导致请求被拒或脏数据,上线前务必在策略测试器跑一遍 Get 出来的 n 是否符合预期。

MQL5 / C++
metadata.lengths.t = class="kw">sizeof(class="type">int);
metadata.lengths.b = ArraySize(body);

class=class="str">"cmt">// 分配结果数组,用于元数据和参数数据
class="type">uchar data[];
ArrayResize(data, LEADSIZE + metadata.total());

class=class="str">"cmt">// 把元数据以字节数组方式放在数组的开始
ArrayCopy(data, metadata.sizes);

class=class="str">"cmt">// 把数据栏位挨个放到数组中
class="type">int cursor = LEADSIZE;
class="type">uchar temp[];
StringToCharArray(method, temp);
ArrayCopy(data, temp, cursor);
ArrayResize(temp, class="num">0);
cursor += metadata.lengths.m;

StringToCharArray(url, temp);
ArrayCopy(data, temp, cursor);
ArrayResize(temp, class="num">0);
cursor += metadata.lengths.u;

StringToCharArray(headers, temp);
ArrayCopy(data, temp, cursor);
ArrayResize(temp, class="num">0);
cursor += metadata.lengths.h;

int2chars.x = timeout;
ArrayCopy(data, int2chars.b, cursor);
cursor += metadata.lengths.t;

ArrayCopy(data, body, cursor);

class=class="str">"cmt">// 在资源中存储数组
resource = data;

class="type">void unpackRequest(class="type">class="kw">string &method, class="type">class="kw">string &url, class="type">class="kw">string &headers, class="type">int &timeout, class="type">uchar &body[])
{
  class="type">uchar array[];
  class=class="str">"cmt">// 使用资源中的数据填充数组  
  class="type">int n = resource.Get(array);
  Print(ChartID(), ": Got ", n, " bytes in request");
  
  class=class="str">"cmt">// 从数组中读取元数据
  ArrayCopy(metadata.sizes, array, class="num">0, class="num">0, LEADSIZE);
  class="type">int cursor = LEADSIZE;
  class=class="str">"cmt">// 一个个读取所有的数据栏位      
  method = CharArrayToString(array, cursor, metadata.lengths.m);
  cursor += metadata.lengths.m;
  url = CharArrayToString(array, cursor, metadata.lengths.u);
  cursor += metadata.lengths.u;
  headers = CharArrayToString(array, cursor, metadata.lengths.h);
  cursor += metadata.lengths.h;
  
  ArrayCopy(int2chars.b, array, class="num">0, cursor, metadata.lengths.t);
  timeout = int2chars.x;
  cursor += metadata.lengths.t;
  
  if(metadata.lengths.b > class="num">0)
  {
    ArrayCopy(body, array, class="num">0, cursor, metadata.lengths.b);
  }
}

class="type">void packResponse(class="kw">const class="type">class="kw">string source, class="kw">const class="type">uchar &result[], class="kw">const class="type">class="kw">string &result_headers);

「用图表事件把 HTTP 响应搬回 EA」

在 MT5 里做异步 Web 请求,难点从来不是发得出去,而是响应怎么绕回发起图表。上面这段 ClientWebWorker 的做法,是把请求参数打包后,用 EventChartCustom 向管理图表的 managerChartID 投一个自定义事件,事件 ID 暂填 0,后续要换成独立标识避免冲突。 request() 里先存下 method 和 url,再借 ResourceMediator 的 packRequest 把头部、超时、body 塞进资源通道;busy 标记直接取 EventChartCustom 的返回值,非零才代表事件投递成功,调用方可以据此判断本轮请求是否真的排上队。 响应回来走 receiveResult() 静态函数:先用 ChartID() 打印当前图表号,再从以 resname 命名的 RESOURCEDATA<uchar> 里取出资源,交给同一个 mediator 做 unpackResponse,把 initiator、headers、text 三个字节数组填回去。你在实盘 EA 里照抄这套,开两个图表分别当管理端和客户端,就能在客户端图表直接 Print 出返回头和内容,验证跨图表通信是否通了。外汇与贵金属行情接口调用受网络与经纪商限制,异步失败概率不低,参数和事件号务必自测。

MQL5 / C++
class="type">void unpackResponse(class="type">uchar &initiator[], class="type">uchar &headers[], class="type">uchar &text[]);
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, class="num">0 class=class="str">"cmt">/* TODO: 特定的信息 */, chartID, class="num">0.0, getFullName());
    class="kw">return busy;
  }
  
  class="kw">static class="type">void receiveResult(class="kw">const class="type">class="kw">string resname, class="type">uchar &initiator[], class="type">uchar &headers[], class="type">uchar &text[])
  {
    Print(ChartID(), ": Reading result ", resname);
    class="kw">const RESOURCEDATA<class="type">uchar> resource(resname);
    ResourceMediator mediator(&resource);
    mediator.unpackResponse(initiator, headers, text);
  }
};

◍ 服务端 Worker 与池化模板怎么写

在管理器(服务端)一侧,我们需要一个从 WebWorker 派生的 ServerWebWorker 类,职责和客户端那边的 ClientWebWorker 近似,但只负责把异步请求分发给辅助 EA 实例。transfer 方法对应交互流程第二步:用客户端传来的 resname(资源名)和 clientChartID(客户图表 ID)做任务派发;receive 方法则在本地 allocate 出资源,并通过 ResourceMediator 把互联网取回的 result 与 result_headers 写进资源,完成第五步的部分动作。 由于客户端可能同时下几个文档,而管理器端一开始最好预开足够多的助手实例,否则多客户并发时容易堵。于是用 WebWorkersPool 这个模板基类来托管对象数组——客户端存 ClientWebWorker,服务端存 ServerWebWorker,类型不同所以用 template<typename T>。构造函数按指定 size 分配数组,析构逐一 delete 动态指针,findWorker / getIdleWorker 按条件检索,operator<< 动态加对象,revoke 动态删。 客户端池因事件处理特殊,又从基类扩出 ClientWebWorkersPool,三个变量要留意:managerChartID 是管理器窗口 ID,managerPoolSize 是初始数组大小,name 是所有资源通用前缀(如 WRP_)。下面代码可直接贴进 MT5 看类结构。

MQL5 / C++
class ServerWebWorker : class="kw">public WebWorker
{
  class="kw">public:
    ServerWebWorker(class="kw">const class="type">long id, class="kw">const class="type">class="kw">string p = "WRP_"): WebWorker(id, p)
    {
    }
    
    class="type">bool transfer(class="kw">const class="type">class="kw">string resname, class="kw">const class="type">long clientChartID)
    {
      class=class="str">"cmt">// 回应 `clientChartID` 的客户端,任务名为 `resname` 
      class=class="str">"cmt">// 并且把任务分配给 ID 为 `chartID` 的工作单元 
      busy = EventChartCustom(clientChartID, TO_MSG(MSG_ACCEPTED), chartID, class="num">0.0, resname)
            && EventChartCustom(chartID, TO_MSG(MSG_WEB), clientChartID, class="num">0.0, resname);
      class="kw">return busy;
    }
    
    class="type">void receive(class="kw">const class="type">class="kw">string source, class="kw">const class="type">uchar &result[], class="kw">const class="type">class="kw">string &result_headers)
    {
      ResourceMediator mediator(allocate());
      mediator.packResponse(source, result, result_headers);
    }
};
class="kw">template<class="kw">typename T>
class WebWorkersPool
{
  class="kw">protected:
    T *workers[];
    
  class="kw">public:
    WebWorkersPool() {}
    
    WebWorkersPool(class="kw">const class="type">uint size)
    {
      class=class="str">"cmt">// 分配工作单元;在客户端它们用于在资源中保存请求参数
      ArrayResize(workers, size);
      for(class="type">int i = class="num">0; i < ArraySize(workers); i++)
      {
        workers[i] = NULL;
      }
    }
    
    ~WebWorkersPool()
    {
      for(class="type">int i = class="num">0; i < ArraySize(workers); i++)
      {
        if(CheckPointer(workers[i]) == POINTER_DYNAMIC) class="kw">delete workers[i];
      }
    }
    
    class="type">int size() class="kw">const
    {
      class="kw">return ArraySize(workers);
    }
    
    class="type">void class="kw">operator<<(T *worker)
    {
      class="kw">const class="type">int n = ArraySize(workers);
      ArrayResize(workers, n + class="num">1);
      workers[n] = worker;
    }
    
    T *findWorker(class="kw">const class="type">class="kw">string resname) class="kw">const
    {
      for(class="type">int i = class="num">0; i < ArraySize(workers); i++)
      {
        if(workers[i] != NULL)
        {

Worker 池的检索与回收接口

这段模板类代码给出了多图表工作线程(worker)池的几个核心查询与销毁方法,直接决定了 EA 在 MT5 里如何复用子图表资源。 getWorkerByName 用全名字符串比对,命中即返回对象指针,否则落空返回 NULL;getIdleWorker 则先确认 ChartPeriod(chart_id) > 0(图表仍存在),再筛 !isBusy() 的空闲实例,适合调度新任务。 findWorker(const long id) 按图表 ID 精确查找,revoke(const long id) 在确认 CheckPointer == POINTER_DYNAMICdelete 并置 NULL,避免野指针。available() 遍历数组统计非空槽位,复杂度 O(ArraySize),在 worker 数超 50 时可能成为微瓶颈。 下标运算符 operator[](int i) 仅作常量指针透传,调用方需自担越界与 NULL 风险。复制进 MT5 后,可先打印 available() 验证池容量是否符合预期。

MQL5 / C++
if(workers[i].getFullName() == resname)
{
  class="kw">return workers[i];
}
}
class="kw">return NULL;
}

T *getIdleWorker() class="kw">const
{
  for(class="type">int i = class="num">0; i < ArraySize(workers); i++)
  {
    if(workers[i] != NULL)
    {
      if(ChartPeriod(workers[i].getChartID()) > class="num">0) class=class="str">"cmt">// check if exist
      {
        if(!workers[i].isBusy())
        {
          class="kw">return workers[i];
        }
      }
    }
  }
  class="kw">return NULL;
}

T *findWorker(class="kw">const class="type">long id) class="kw">const
{
  for(class="type">int i = class="num">0; i < ArraySize(workers); i++)
  {
    if(workers[i] != NULL)
    {
      if(workers[i].getChartID() == id)
      {
        class="kw">return workers[i];
      }
    }
  }
  class="kw">return NULL;
}

class="type">bool revoke(class="kw">const class="type">long id)
{
  for(class="type">int i = class="num">0; i < ArraySize(workers); i++)
  {
    if(workers[i] != NULL)
    {
      if(workers[i].getChartID() == id)
      {
        if(CheckPointer(workers[i]) == POINTER_DYNAMIC) class="kw">delete workers[i];
        workers[i] = NULL;
        class="kw">return true;
      }
    }
  }
  class="kw">return class="kw">false;
}

class="type">int available() class="kw">const
{
  class="type">int count = class="num">0;
  for(class="type">int i = class="num">0; i < ArraySize(workers); i++)
  {
    if(workers[i] != NULL)
    {
      count++;
    }
  }
  class="kw">return count;
}

T *class="kw">operator[](class="type">int i) class="kw">const
{

「客户端工作池怎么接管理器的回包」

在 MT5 里做跨图表 WebRequest 异步调用,客户端池 ClientWebWorkersPool 继承自 WebWorkersPool<T>,构造时靠 WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID()) 向管理器图表广播发现消息,自身并不直连网络。 WebRequestAsync 先抓空闲 worker,命中后把请求转给 managerChartID 指定的管理器图表;若池内无空闲 worker 直接返回 false,这意味着并发上限由池 size 决定,超量请求会被静默丢弃。 图表事件里 MSG_DONE 代表异步结束:dparam 是结果码,sparam 是资源名,用 ClientWebWorker::receiveResult 从资源读出 initiator/headers/text,再按 initiator 名找回原 worker 调 onResultreleaseMSG_HELLO 是发现阶段的回包:当 managerChartID==0lparam!=0 时,用 ChartPeriod(lparam)>0 校验图表有效性后记录管理器 ID 与池大小,后续请求才有投递目标。外汇与贵金属行情接口调用受经纪商服务器限制,异步失败概率随并发升高而上升,需实测池大小。

MQL5 / C++
class="kw">template<class="kw">typename T>
class ClientWebWorkersPool: class="kw">public WebWorkersPool<T>
{
class="kw">protected:
  class="type">long   managerChartID;
  class="type">short  managerPoolSize;
  class="type">class="kw">string name;

class="kw">public:
  ClientWebWorkersPool(class="kw">const class="type">uint size, class="kw">const class="type">class="kw">string prefix): WebWorkersPool(size)
  {
    name = prefix;
    class=class="str">"cmt">// 尝试寻找 WebRequest 管理器图表
    WebWorker::broadcastEvent(TO_MSG(MSG_DISCOVER), ChartID());
  }

  class="type">bool WebRequestAsync(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="type">int timeout, class="kw">const class="type">char &data[])
  {
    T *worker = getIdleWorker();
    if(worker != NULL)
    {
      class="kw">return worker.request(method, url, headers, timeout, data, managerChartID);
    }
    class="kw">return class="kw">false;
  }

  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_DONE) class=class="str">"cmt">// 异步请求完成,有结果或者出错
    {
      Print(ChartID(), ": Result code ", (class="type">long)dparam);

      if(sparam != NULL)
      {
        class=class="str">"cmt">// 从资源中根据 sparam 中的名称读取数据
        class="type">uchar initiator[], headers[], text[];
        ClientWebWorker::receiveResult(sparam, initiator, headers, text);
        class="type">class="kw">string resname = CharArrayToString(initiator);

        T *worker = findWorker(resname);
        if(worker != NULL)
        {
          worker.onResult((class="type">long)dparam, headers, text);
          worker.release();
        }
      }
    }

    ...

    else
    if(MSG(id) == MSG_HELLO) class=class="str">"cmt">// MSG_DISCOVER 广播结果找到了管理器
    {
      if(managerChartID == class="num">0 && lparam != class="num">0)
      {
        if(ChartPeriod(lparam) > class="num">0)
        {
          managerChartID = lparam;
          managerPoolSize = (class="type">short)dparam;
          for(class="type">int i = class="num">0; i < ArraySize(workers); i++)

◍ 用 ChartID 给工作线程贴标签

上面这段把管理器图表 ID 和序号拼进对象名,本质是给每个 worker 一个可回溯的身份证。ChartID() 返回当前图表唯一整数,name 加 i+1 再加下划线,能在 MT5 终端里直接搜到对应实例。 isManagerBound() 只做一件事:managerChartID 非 0 才认为绑定成功。这个 0 值判断是哨兵,没拿到合法图表 ID 的 manager 后续逻辑不该跑。 开 MT5 新建 EA 把这段塞进类定义,编译后看 experts 日志里 name_1_、name_2_ 的构造顺序,能确认多实例是不是按预期起得来。外汇和贵金属品种波动大,这类图表对象若没随图表关闭释放,可能拖慢终端。

MQL5 / C++
    workers[i] = new T(ChartID(), name + (class="type">class="kw">string)(i + class="num">1) + "_");
    }
      }
        }
          }
            }

  class="type">bool isManagerBound() class="kw">const
  {
    class="kw">return managerChartID != class="num">0;
  }
};
把辅助图表调度交给小布
小布盯盘的 AIGC 已内置多品种线程健康度诊断,打开对应品种页即可看到助手池是否掉线。把重复劳动交给小布,你专注决策。

常见问题

WebRequest 是同步函数,执行期间当前线程的控制权不交还内核,后续分时、深度事件只能排队或跳过。
通常通过全局变量或文件映射,由管理器统一收发,主 EA 轮询或经事件通知读取。
可以,小布盯盘的品种页内置线程与助手池诊断,能提示掉线或积压,省去手动查日志。
首个运行的通用 EA 实例声明自己为管理器,后续实例按优先级降为助手,由管理器开图表。
是,每个 EA 在独立图表分配单独线程,一次只跑一个代码段,其余事件排队。