在 Linux 上利用 C++ 多线程支持开发 MetaTrader 5 概念验证 DLL·进阶篇
🐧

在 Linux 上利用 C++ 多线程支持开发 MetaTrader 5 概念验证 DLL·进阶篇

(2/3)· 在 Ubuntu 上用 C++ 多线程造出 Windows 能加载的 DLL,省掉双系统切换的折腾成本

含代码示例实战向 第 2/3 篇
不少开发者卡在「想在 Linux 写代码,却要交付 Windows 能跑的 MT5 库」这道坎上。靠纯 MQL5 扩不出系统级线程能力,而交叉编译 DLL 的工具链坑位没人指路就容易半途放弃。本篇接着上篇环境铺垫,直接把 Mingw 与多线程构建跑通。

「把 GCC 编译习惯平移到 Windows 的工具链」

Mingw 是 GNU 编译器集合(GCC)在 Windows 平台的移植版,全称 Minimalist GNU for Windows,底层仍是 GCC 工具链。它的编译标志与 GCC 保持一致,用法也高度相似,所以长期在 Linux 下写 C/C++ 的人,基本不用重新学一套参数就能直接在 Windows 上出包。 更省心的是,GCC 的编译选项本身和 Clang 也很接近,三种编译器之间切换时,知识可以无损迁移。对需要在 MT5 外部模块或共享库开发上跨系统协作的交易工具开发者,这意味着同一份源码能在 Linux 和 Windows 两端用近似命令构建。 下面这段对比给出了同一份 example.cpp 在两种环境下的实际编译命令:GCC 产出 Linux 的 .so,Mingw 产出 Windows 的 .dll,都要求 C++17 与位置无关代码(-fPIC),并链接 pthread。外汇与贵金属相关的外部 DLL 接入存在平台兼容与高危执行风险,实盘前务必在模拟环境验证。

MQL5 / C++
g++ -shared -std=c++class="num">17 -fPIC -o libexample.so example.cpp -lpthread
x86_64-w64-mingw32-g++-posix -shared -std=c++class="num">17 -fPIC -o example.dll example.cpp -lpthread

◍ Mingw 下三种线程模型的编译差异

把 C++ 源码编成可执行库,GCC 与 Mingw 的命令行几乎一致:核心都是 -std=c++17 -I. -o 输出 源文件 -L. -l库名,差别只在调用的编译器二进制不同。 Mingw 提供 3 种变体,直接牵出线程实现方式的分叉:x86_64-w64-mingw32-g++-posix 配合 pthread,x86_64-w64-mingw32-g++-win32(别名就是 x86_64-w64-mingw32-g++)走 win32 API 线程模型。选错变体,链接阶段就可能报线程符号缺失。 除编译前端外,工具链还带前缀工具:x86_64-w64-mingw32-gcc-nm 做符号整理,x86_64-w64-mingw32-gcc-ar 管存档,x86_64-w64-mingw32-gcc-gprof 出性能剖析。部分工具也分 -posix / -win32 后缀变体,构建脚本里要写全。 实机上跑一遍 x86_64-w64-mingw32-g++-posix -std=c++17 -I. -o main.exe main.cpp -L. -lexample,再换 -win32 重编,能用 MT5 外部调用确认哪套线程模型更稳。

MQL5 / C++
x86_64-w64-mingw32-...

Mingw 下两套线程模型别混用

Mingw 在 Windows 上给了两种线程实现:POSIX(pthread)和 Win32。写 EA 配套的 DLL 时,这套选择直接牵动兼容性与延迟,不能当背景噪音忽略。 如果你的代码同时碰了 C++ 标准多线程(std::thread、std::promise)和操作系统原生接口(Win32 的 CreateThread、POSIX 的 pthread_create),最好从头到尾只认一个 API 家族。除非遇到 C++ 标准库确实给不出、只有系统 API 才有的能力,否则把两套混在一个工程里,容易踩未定义行为和链接冲突的坑。 用 pthread 实现就别手痒调 Win32 线程;反过来用 Win32 线程(mingw 的 win32 变体)就离 pthread API 远点。保持单一模型,是少掉头发的前提。 性能上两套有差异,后面基准章节会跑数据,但结论先放这:低延迟场景里某一套可能更合算,具体看实测。我们接下来先做概念验证 DLL 和测试程序,再对两种实现跑分。 可移植代码会同时兼容 pthread 和 win32,构建系统一键切换。若选 win32 线程,要从 mingw-std-threads 项目拉头文件,这一步后面会指给你。外汇/贵金属相关的 DLL 接入本就高风险,线程模型选错可能让 MT5 终端莫名卡死,动手前先定好用哪套。

「在 Linux 上搭好 Wine 与交叉编译环境」

想在 Linux 下跑 MT5 做 MQL5 外部程序联调,第一步是把 Wine 和 Mingw 工具链装对。先装 winehq-devel 开发包而不是 stable 版,否则遇到构建 3550 在 Wine 下启动崩溃的问题时,官方安装脚本会直接用 stable 覆盖掉你的 devel 环境。装完用 wine --version 自测,正常应看到类似 wine-8.0-rc3 的输出版本号。 MT5 本身若已装过,不要直接跑官方脚本(它建在 ~/.mt5 前缀里且会强塞 stable 包)。在 ~/.bash_aliases 里写一行 alias mt5trader="WINEPREFIX=~/.mt5 wine '/home/haxpor/.mt5/drive_c/Program Files/MetaTrader 5/terminal64.exe'",source 后再敲 mt5trader 启动,终端会直接吐出调试输出,后面从概念验证程序抓日志就不需要在代码里塞复杂输出了。 交叉编译靠 mingw-w64:sudo apt install mingw-w64 后系统多出一堆 x86_64-w64-mingw32- 前缀工具,常用的是 x86_64-w64-mingw32-g++-posix。若用 win32 线程模型则换 x86_64-w64-mingw32-win32。 mingw-std-threads 只是头文件方案,把 Kitware 的仓库克隆下来,建 /usr/x86_64-w64-mingw32/include/mingw-std-threads 目录,把 .h 全拷进去即可。之后代码里用到 <mutex> <thread> 等多线程头,要改成从 mingw-std-threads 路径包含,例如 <mutex> 换 <mingw-std-threads/mingw.mutex.h>,否则在 Wine 里链接会出问题。外汇与贵金属交易本身高风险,环境没调通就急着上实盘 EA 更容易踩坑。

MQL5 / C++
sudo apt install winehq-devel

◍ 用 C++17 给 MT5 写多线程 DLL 的坑与验证

概念验证先从 DLL 结构落地:example.h / example.cpp 供 MT5 调用,main.cpp 做原生 Linux 测试,三套 Makefile 分别覆盖 pthread 交叉编译、win32 线程交叉编译、以及本机 g++ 调试。交叉编译时务必带 -DWINDOWS -DEXAMPLE_EXPORT,否则 MT5 端找不到导出符号。 头文件里 extern "C" 是硬要求:一旦走 C++ 命名空间或自由函数,MQL5 加载 DLL 时链接器就匹配不到签名。Windows 侧用 __declspec(dllexport/dllimport) 导出导入,Linux 下 EXAMPLE_API 留空即可。 性能对照来自作者 6 核 12 线程机器:multi_threaded_sum_v2 比 single_threaded_sum 快约 4.33 倍。v1 曾给每线程拷一份数据,因现代 CPU 的 MESI 缓存一致性协议已能共享缓存行,v1 反而比 v2 慢 2–5 倍——所以 v2 直接让线程操作原数组、用局部变量汇总避免伪共享,再用 std::atomic 做线程安全累加。 构建共享库关键旗:-fPIC 生成位置无关代码,-fno-rtti 关掉运行时类型信息省开销,-O2 开二级优化,-lpthread 链线程库。本机跑通后再用 Makefile 交叉出 example.dll 和 main.exe,但 main.exe 启动报错找不到 DLL,需把 libgcc_s_seh-1.dll、libstdc++6.dll、libwinpthread-1.dll 拷到同目录;用 pthread 构建就从 mingw 的 posix 目录取,用 win32 线程则从 win32 目录取。 DLOG() 受 Android 日志启发,编译期加 -DENABLE_DEBUG 才生效,不影响无调试时的速度与二进制体积。复杂场景别让 MQL5 调用阻塞等结果,后续可拆出发请求/取结果两个接口走 std::future,这篇先不展开。

把 DLL 接口接到 MQL5 前的头文件写法

前面测过,多线程求和比单线程快约 3.4 倍,但仍略慢于原生 Linux 构建,这个差距在 Windows 下用 MinGW 编译 DLL 时基本不可避免。 接下来要做的,是把 C++ 计算逻辑包成 DLL,再让 MT5 里的 MQL5 直接调。头文件里最关键的一点是必须用 extern "C" 包裹导出函数,否则 MQL5 按名字找不到入口,加 namespace 也没用。 Windows 下靠 __declspec(dllexport/dllimport) 控制导出导入,Linux 下留空即可;add、sub 这类简单函数只是验证调用链,真正的重活是 single_threaded_sum 与 multi_threaded_sum_v2,后者按 MESI 缓存一致性协议共享只读数组,避免拷贝开销。 编译时建议开 ENABLE_DEBUG 用 DLOG 打点,能看到各线程耗时;等 MQL5 侧写完并编译,再跑一次同样的数组规模基准,才能确认 MT5 调用层的真实损耗。

MQL5 / C++
class="macro">#pragma once
class="macro">#ifdef WINDOWS
			class="macro">#ifdef EXAMPLE_EXPORT
				class="macro">#define EXAMPLE_API __declspec(dllexport)
			class="macro">#else
				class="macro">#define EXAMPLE_API __declspec(dllimport)
			class="macro">#endif
class="macro">#else
			class="macro">#define EXAMPLE_API
class="macro">#endif
class=class="str">"cmt">// we have to use &class="macro">#x27;class="kw">extern "C"&class="macro">#x27; in order to class="kw">export functions from DLL to be used
class=class="str">"cmt">// in MQL5 code.
class=class="str">"cmt">// Using &class="macro">#x27;class="kw">namespace&class="macro">#x27; or without such class="kw">extern won&class="macro">#x27;t make it work for MQL5 code, it
class=class="str">"cmt">// won&class="macro">#x27;t be able to find such functions.
class="kw">extern "C" {
class=class="str">"cmt">/**
* Add two specified number together.
*/
			EXAMPLE_API [[nodiscard]] class="type">int add(class="type">int a, class="type">int b) noexcept;
class=class="str">"cmt">/**
* Subtract two specified number.
*/
			EXAMPLE_API [[nodiscard]] class="type">int sub(class="type">int a, class="type">int b) noexcept;
class=class="str">"cmt">/**
* Get the total number of hardware&class="macro">#x27;s concurrency.
*/
EXAMPLE_API [[nodiscard]] class="type">int num_hardware_concurrency() noexcept;
class=class="str">"cmt">/**
* Sum all elements from specified array for number of specified elements.
* The computation will be done in a single thread linearly manner.
*/
EXAMPLE_API [[nodiscard]] class="type">int single_threaded_sum(class="kw">const class="type">int arr[], class="type">int num_elem);
class=class="str">"cmt">/**
* Sum all elements from specified array for number of specified elements.
* The computation will be done in a multi-thread.
*
* This version is suitable for processor that bases on MESI cache coherence
* protocol. It won&class="macro">#x27;t make a copy of class="kw">input array of data, but instead share
* it among all threads for reading purpose. It still attempt to write both
* temporary and final result with minimal number of times thus minimally
* affect the performance.
*/
EXAMPLE_API [[nodiscard]] class="type">int multi_threaded_sum_v2(class="kw">const class="type">int arr[], class="type">int num_elem);
};
class="macro">#include "example.h"
class="macro">#ifdef USE_MINGW_STD_THREAD
			class="macro">#include <mingw-std-threads/mingw.thread.h>
class="macro">#else
			class="macro">#include <thread>
class="macro">#endif
class="macro">#include <iostream>
class="macro">#include <vector>
class="macro">#include <chrono>
class="macro">#include <numeric>
class="macro">#include <atomic>
class="macro">#ifdef ENABLE_DEBUG
class="macro">#include <cstdarg>
class="macro">#endif
class="macro">#ifdef ENABLE_DEBUG
class="kw">const class="type">int LOG_BUFFER_SIZE = class="num">2048;
class="type">char log_buffer[LOG_BUFFER_SIZE];
class="kw">inline class="type">void DLOG(class="kw">const class="type">char* ctx, class="kw">const class="type">char* format, ...) {
			va_list args;
			va_start(args, format);
			std::vsnprintf(log_buffer, LOG_BUFFER_SIZE-class="num">1, format, args);
			va_end(args);
			std::cout << "[DEBUG] [" << ctx << "] " << log_buffer << std::endl;
}
class="macro">#else
			class="macro">#define DLOG(...)
class="macro">#endif
EXAMPLE_API class="type">int add(class="type">int a, class="type">int b) noexcept {
			class="kw">return a + b;
}
EXAMPLE_API class="type">int sub(class="type">int a, class="type">int b) noexcept {
			class="kw">return a - b;
}
EXAMPLE_API class="type">int num_hardware_concurrency() noexcept {
			class="kw">return std::thread::hardware_concurrency();
}
EXAMPLE_API class="type">int single_threaded_sum(class="kw">const class="type">int arr[], class="type">int num_elem) {
			auto start = std::chrono::steady_clock::now();
			class="type">int local_sum = class="num">0;
把交叉编译链路交给小布盯盘巡检
这些编译与线程诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到构建异常提示,你专注把 C++ 逻辑写对就行。

常见问题

posix 线程模型对 C++11 起的标准线程支持更完整,本文示例采用 x86_64-w64-mingw32-g++-posix 来避免原生 win32 线程缺失导致的问题。
可以,小布盯盘内置的 AIGC 诊断能标记 EA 调用外部库时的异常,减少你手动查 Wine 日志的时间。
该版本在 winehq-stable 上启动即崩溃,文中改用 winehq-devel 8.0-rc3 才正常,实战里优先试 devel 分支。
它链接当前目录下的 libexample 库,配合 -L. 指定搜索路径,是概念验证 DLL 被主程序调用的关键一环。