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

Linux 拥有充满活力的开发生态系统,以及良好的软件开发工效。

它对许多用户极具吸引力,譬如那些喜欢命令行操控,通过软件包管理器轻松安装应用程序,操作系统本身不是黑盒,故您能够深入了解其内部,几乎可以针对所有子系统进行配置,开箱即用的基本开发工具,适合软件开发的灵活和简化的环境,等等。

它的范围从 PC 桌面最终用户,到云解决方案(例如 VPS),或云服务提供商(如 AWS,谷歌云)。

故此,我坚信这里有一些开发人员希望守护他们选择的操作系统,但亦希望能够为 Windows 用户开发和交付产品。 当然,产品必须能够跨平台无缝运行。

通常,MetaTrader 5 开发人员只需利用 MQL5 编程语言来开发他们的指标/智能交易或相关产品,然后在市场上发布给最终用户,而无需担心基于哪个操作系统。 他们可以依靠 MT5 的 IDE 来负责编译和构建交付前的(.EX5)可执行文件(前提是他们知道如何在 Linux 上启动 MetaTrader 5)。 但是,当开发人员需要将自定义解决方案开发为共享库(DLL),以便进一步扩展和提供仅靠 MQL5 编程语言无法提供的其它服务时,他们将不得不花费更多的时间和精力来寻求交叉编译的解决方案、发现漏洞和最佳实践、熟悉工具,等等。


这些就是本文出现的原因。 依靠涉及交叉编译解决方案,并利用 C++ 具有的多线程能力构建 DLL,这两者相结合,至少可作为开发人员进一步扩展的基础。 我希望它能帮助您在心仪的操作系统(即 Linux)上继续开发 MetaTrader 5 相关产品。
 

本文是为哪些人准备的

我假设阅读本文的读者都已经有一些通过命令行与 Linux 交互的经验,并且拥有在 Linux 上编译和构建 C++ 源代码的一般概念。

无论如何,本文是为那些想要探索开发步骤和工作流程的人员准备的,以便能够开发在 Linux 以及 Windows 上都能工作的,具有多线程功能的 DLL。 扩展线程编程选项,不仅内置 OpenCL,而且具有 C++ 多线程功能,可与一些紧密基于它的其它系统集成,从而扩展线程编程选项。 
 

所用系统 & 软件

  • Ubuntu 20.04.3 LTS 内核版本 5.16.0 基于 AMD Ryzen 5 3600 6-核处理器 (每核 2 线程), 32 GB 内存
  • Wine (winehq-devel 软件包) 版本 8.0-rc3 (截至撰写本文时) - (另请参阅 MT5 构建版 3550 于 winehq-stable 软件包上启动时立即崩溃,这就是为何本文决定采用 devel 版本软件包,而非 stable 版)
  • Mingw (mingw-w64 软件包) 版本 7.0.0-2
  • Virtualbox 版本 6.1 用于在 Windows 系统上测试
     

游戏计划

我们将基于以下计划

  1. 了解 Wine
  2. 了解 Mingw
  3. Mingw 的线程实现
    1. POSIX (pthread)
    2. Win32 (经由 mingw-std-threads)
  4. 准备 Linux 开发机器
    1. Wine 安装
    2. MetaTrader 5 安装
    3. Mingw 安装
    4. (可选)mingw-std-threads 安装
  5. 概念验证,开发阶段 I — DLL(C++ 多线程支持)
  6. 概念验证,开发阶段 II — 调用 DLL 的 MQL5 代码
  7. 在 Windows 系统上测试
  8. Mingw 线程实现的简单基准测试

Wine

Wine 是缩写 (技术上说是 递归 回旋缩写) "Wine is Not an Emulator"。 它并非任何处理器或目标硬件的模拟器。 取而代之,它是在非 Windows 操作系统上运行的 win32 API 的包装器。

Wine 引入了另一个抽象层,该抽象层拦截来自非 Windows 系统上的用户对 win32 API 的调用,然后将其路由到 Wine 的内部,然后其处理和行为请求,就如同(或几乎以相同的方式)在 Windows 上的行为一样。 

这意味着调用这些 win32 API 的 Wine 操作是基于 POSIX API 运行。 读者也许体验过 Wine 软件,却不知道它们何时在 Linux 上启动此类 Windows 软件,甚至可以在 Linux 上玩 steam 游戏,因为其运行时基于称为 Proton 的 Wine 变体。

这允许灵活地测试或使用 Windows 软件,其替代方案能否在 Linux 上可用。

通常,当您想通过 Wine 运行基于 Windows 的应用程序时,您将执行以下命令

wine windows_app.exe

或者,如果您想与特定 Wine 环境前缀关联,来运行应用程序,您也可以这样做

WINEPREFIX=~/.my_mt5_env wine mt5_terminal64.exe

Mingw

Mingw 代表 "Minimalist GNU for Windows"。 它是 GNU 编译器集合(GCC)及其工具链的端口,用于在 Linux 上编译针对 Windows 的 C/C++ 和其它一些编程语言。

功能上,在 GCC 和 Mingw 中编译标志/选项一致,且用法相似,因此用户可以轻松地将他们现有的知识从 GCC 转换为 Mingw。 另请注意,GCC 在编译标志/选项上也与 Clang 非常相似。 故此,您可以看到用法无缝转换,用户能够保留他们的知识,能够将用户群扩展到 Windows 系统上。

请参阅以下比较表以了解差异。

  • 编译 C++ 源代码,并构建共享库
编译器命令行
GCC
g++ -shared -std=c++17 -fPIC -o libexample.so example.cpp -lpthread
Mingw
x86_64-w64-mingw32-g++-posix -shared -std=c++17 -fPIC -o example.dll example.cpp -lpthread

  • 编译 C++ 源代码,并构建可执行库
编译器命令行
GCC
g++ -std=c++17 -I. -o main.out main.cpp -L. -lexample
Mingw 
x86_64-w64-mingw32-g++-posix -std=c++17 -I. -o main.exe main.cpp -L. -lexample

读者能注意到差异很小。 编译标志非常相似,基本相同。 仅在于我们所用的编译器二进制文件有差异,并以此来编译和构建我们需要的所有东西。

有 3 种变体可供使用,其会引发线程实现的主题,我们将在下一节中对其进行解释。

  1. x86_64-w64-mingw32-g++
    它别名为  x86_64-w64-mingw32-g++-win32
     
  2. x86_64-w64-mingw32-g++-posix
    其二进制可执行文件打算与 pthread 配合使用。
     
  3. x86_64-w64-mingw32-g++-win32
    其二进制可执行文件旨在与 win32 API 线程模型配合使用。 它别名为 86_64-w64-mingw32-g++

此外,还有其它几个工具前缀为 

x86_64-w64-mingw32-...

一些示例如下

  • x86_64-w64-mingw32-gcc-nm - 名称整理工具
  • x86_64-w64-mingw32-gcc-ar - 存档管理工具
  • x86_64-w64-mingw32-gcc-gprof - 类 Unix 操作系统的性能分析工具

此外还有 x86_64-w64-mingw32-gcc-nm-posix和 x86_64-w64-mingw32-gcc-nm-win32 针对某些版本的变体。

Mingw 线程实现

从上一章节中,我们现在知道 Mingw 提供了 2 种线程实现变体。

  1. POSIX (pthread)
  2. Win32

为什么我们需要如此关注这一点? 我能想到的两个原因

  1. 出于安全性和兼容性
    当您的代码有潜力同时使用 C++ 多线程能力(例如 std::thread, std::promise,等等),以及操作系统的原生多线程支持(例如 win32 API 支持的 CreateThread()  ,和 POSIX API 支持的 pthread_create() ,如此最好坚持只用其中一个 API,而不必换到另一个。

    无论如何,我们不太可能在代码里把来自 C++ 的多线程功能,和操作系统本身的支持混合在一起,除非出现非常特殊的情况,即操作系统支持 API 的提供了更多 C++ 无法提供的功能。 故此,最好保持一致性,并针对两者运用相同的线程模型。
    如果我们使用 Mingw 的 pthread 实现,那么尽量不要使用 win32 API 的线程功能。 同样,如果我们使用 Mingw 的 win32 线程实现(从现在开始简称 “win32 线程”),那么最好避免使用 OS 的 pthread API。
  2. 性能 (稍后请参阅 Mingw 线程实现的简单基准测试章节)
    当然,用户想要一个低延迟的多线程解决方案。 在某些情况下更快的执行解决方案,可能是某些用户的选择之一。

我们将首先开发概念验证 DLL 和测试程序,然后再针对两个线程实现进行基准测试。

针对该项目,我们提供的可移植代码可采用 pthread 或 win32 线程,我们的构建系统能够轻松地相互切换。
在使用 win32 线程的情况下,我们需要从 mingw-std-threads 项目中安装头文件,我们会在其中指导读者下一步如何操作。

准备 Linux 开发计算机

在直接进入编码部分之前,我们需要先安装所需的软件。

Wine 安装

执行以下命令以安装 Wine devel 软件包。

sudo apt install winehq-devel

然后用以下命令检查它是否能正常工作,

wine --version

其输出应是这样的

wine-8.0-rc3

MetaTrader 5 安装

大多数用户早在构建版本3550之前就已经安装了MetaTrader 5,而该构建版本存在崩溃问题。 为了使用 winehq-devel 软件包来解决问题,并能够启动 MetaTrader 5,我们不能直接使用如同如何在 Linux 上安装平台所示的官方安装脚本。
最好能自己执行命令,因为直接执行官方安装脚本会用 stable 版本软件包覆盖我们的 Wine。

我已经撰写了指南 MT5 构建 3550 在 Linux 上通过 Wine 启动时崩溃。 如何解决?  该篇文章应该涵盖已安装 Wine stable 版本软件包用户的所有情况,或者想要从 devel 包重新开始的用户。

如此之后,尝试再次通过 Wine 启动 MetaTrader 5。 看看是否有任何问题。

注意

官方安装脚本将在 ~/.mt5 处创建一个 Wine 环境(称为前缀)。 在您的 ~/.bash_aliases 中包含以下行可能很方便,这样您就可以轻松启动 MetaTrader 5。

alias mt5trader="WINEPREFIX=~/.mt5 wine '/home/haxpor/.mt5/drive_c/Program Files/MetaTrader 5/terminal64.exe'"

然后执行命令 source

source ~/.bash_aliases

最后执行以下命令来启动 MT5,其调试输出将显示在终端上。

mt5trader

按这种方式启动 MetaTrader 5 将允许我们以后轻松地从概念验证应用程序中查看调试日志,而不会令我们的代码变得复杂。

Mingw 安装

执行以下命令以安装 Mingw。

sudo apt install mingw-w64

这会将一堆工具的集合安装到您的系统中,其中这些工具的前缀为 x86_64-w64-mingw32- 大多数情况下,我们将使用 x86_64-w64-mingw32-g++-posix (或 x86_64-w64-mingw32-win32 如若使用 win86 线程)。

mingw-std-threads 安装

mingw-std-threads 是一个将 Mingw 的 win32 线程粘合到 Linux 上的项目。 它仅提供头文件作为插入式解决方案。 因此,安装很简单,只需要将其头文件放入系统的包含路径之中。

请遵照以下步骤进行安装。

首先,将 git 存储库克隆到您的系统当中。

git clone git@github.com:Kitware/CMake.git

然后创建一个目录,以便将其头文件保存在系统的包含路径处。

sudo mkdir /usr/x86_64-w64-mingw32/include/mingw-std-threads

最后,将所有头文件(.h)从克隆项目的目录里复制到此类新创建的目录。

cp -av *.h /usr/x86_64-w64-mingw32/include/mingw-std-threads/

这就是全部。 然后在代码中,如果我们决定使用 win32 线程,对于一些与多线程功能相关的头文件(例如线程、同步原语等),我们需要以替换后的名称从正确路径包含它。 有关详尽的清单,请参阅下表。

C++11 多线程头文件包含mingw-std-threads 头文件包含更改为

#include <mutex>

#include <mingw-std-threads/mingw.mutex.h>

#include <thread>

#include <mingw-std-threads/mingw.thread.h>

#include <shared_mutex>

#include <mingw-std-threads/mingw.shared_mutex.h>

#include <future>

#include <mingw-std-threads/mingw.future.h>

#include <condition_variable>

#include <mingw-std-threads/mingw.condition_variable.h>

概念验证,开发阶段 I — DLL(C++ 多线程支持)

现在到了开始编写代码的时候了。

我们的目标是实现一个概念验证 DLL 解决方案,该解决方案能够使用 C++11 标准库中的多线程功能,如此读者可以领会其思想,并进一步扩展。

以下是我们的库和应用程序结构。

项目结构

  • DLL
    • example.cpp
    • example.h
  • 消费者
    • main.cpp
  • 构建系统
    • Makefile- Mingw 用到的 pthread 交叉编译构建文件
    • Makefile-th_win32 - Mingw 用到的 win32 线程交叉编译构建文件 
    • Makefile-g++ - 在原生 Linux 上测试的构建文件。 这是为了在开发项目时进行快速迭代和调试。

采用的 C++ 标准

我们将采用 C++17 标准,尽管我们将主要使用 C++11 中的功能,但一些例如代码注释的属性(如 [[nodiscard]] )需要 C++17。

DLL

example.h

#pragma once

#ifdef WINDOWS
        #ifdef EXAMPLE_EXPORT
                #define EXAMPLE_API __declspec(dllexport)
        #else
                #define EXAMPLE_API __declspec(dllimport)
        #endif
#else
        #define EXAMPLE_API
#endif

// we have to use 'extern "C"' in order to export functions from DLL to be used
// in MQL5 code.
// Using 'namespace' or without such extern won't make it work for MQL5 code, it
// won't be able to find such functions.
extern "C" {
	/**
	 * Add two specified number together.
	 */
        EXAMPLE_API [[nodiscard]] int add(int a, int b) noexcept;

	/**
	 * Subtract two specified number.
	 */
        EXAMPLE_API [[nodiscard]] int sub(int a, int b) noexcept;

	/**
	 * Get the total number of hardware's concurrency.
	 */
	EXAMPLE_API [[nodiscard]] int num_hardware_concurrency() noexcept;

	/**
	 * 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]] int single_threaded_sum(const int arr[], int num_elem);

	/**
	 * 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't make a copy of 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]] int multi_threaded_sum_v2(const int arr[], int num_elem);
};

#pragma once 虽然不是 C++ 标准的一部分,但它得到了 GCC 的支持,因此也支持 Mingw。 这是一种灵活且更简洁的方式来防止包含重复的头文件。 如果不使用此类指令,用户则需同时使用 #ifdef 和 #define,并且需要确保每个定义对每个头文件都有唯一的名称。 这可能很耗时。

我们 #ifdef WINDOWS 来保护定义声明 EXAMPLE_API。 这允许我们能够使用 Mingw 和本机 Linux 系统进行编译。 故此,每当我们针对共享库进行交叉编译时,我们都会将 -DWINDOWS 和 -DEXAMPLE_EXPORT 添加到编译标志当中,否则如果我们只针对测试主程序进行编译,那么我们可以删除 -DEXAMPLE_EXPORT

__declspec(dllexport) 是从 DLL 导出函数的指令

__declspec(dllimport) 是从 DLL 导入函数的指令

编译需要上述两个,以便在 Windows 上使用 DLL。 对于非 Windows 系统,我们不需要它们,但仍需要交叉编译。 因此,如果没有定义用于 Linux 编译的 WINDOWS,则 EXAMPLE_API 为空。

接下来,是函数签名中技术含量最重的部分。 函数签名需要与 C(编程语言)调用约定兼容。 该 extern "C" 将防止函数签名被擅改为 C++ 调用约定。

我们不能将函数签名包装在命名空间中,也不能将它们声明为自由函数,因为 MQL5 代码在以后使用 DLL 时将无法找到这些签名。

对于 num_hardware_concurrency(),它将返回实现支持的并发线程数量。 例如,我使用 6 核处理器,每个内核有 2 个线程,因此它实际上有 12 个线程可以并发工作。 就我的情况,它将返回 12。

single_threaded_sum() 和 multi_threaded_sum_v2() 两者都是我们概念验证应用程序的主要示例,它们展示了多线程的好处,并比较了两者之间的性能。

example.cpp

#include "example.h"

#ifdef USE_MINGW_STD_THREAD
        #include <mingw-std-threads/mingw.thread.h>
#else
        #include <thread>
#endif

#include <iostream>
#include <vector>
#include <chrono>
#include <numeric>
#include <atomic>

#ifdef ENABLE_DEBUG
#include <cstdarg>
#endif

#ifdef ENABLE_DEBUG
const int LOG_BUFFER_SIZE = 2048;
char log_buffer[LOG_BUFFER_SIZE];

inline void DLOG(const char* ctx, const char* format, ...) {
        va_list args;
        va_start(args, format);
        std::vsnprintf(log_buffer, LOG_BUFFER_SIZE-1, format, args);
        va_end(args);

        std::cout << "[DEBUG] [" << ctx << "] " << log_buffer << std::endl;
}
#else
        #define DLOG(...)
#endif

EXAMPLE_API int add(int a, int b) noexcept {
        return a + b;
}

EXAMPLE_API int sub(int a, int b) noexcept {
        return a - b;
}

EXAMPLE_API int num_hardware_concurrency() noexcept {
        return std::thread::hardware_concurrency();
}

EXAMPLE_API int single_threaded_sum(const int arr[], int num_elem) {
        auto start = std::chrono::steady_clock::now();

        int local_sum = 0;
        for (int i=0; i<num_elem; ++i) {
                local_sum += arr[i];
        }

        std::chrono::duration<double, std::milli> exec_time = std::chrono::steady_clock::now() - start;
        std::cout << "elapsed time: " << exec_time.count() << "ms" << std::endl;
        return local_sum;
}

EXAMPLE_API int multi_threaded_sum_v2(const int arr[], int num_elem) {
        auto start = std::chrono::steady_clock::now();

        std::vector<std::pair<int, int>> arr_indexes;

        const int num_max_threads = std::thread::hardware_concurrency() == 0 ? 2 : std::thread::hardware_concurrency();
        const int chunk_work_size = num_elem / num_max_threads;

        std::atomic<int> shared_total_sum(0);

        // a lambda function that accepts input vector by reference
        auto worker_func = [&shared_total_sum](const int* arr, std::pair<int, int> indexes) {
                int local_sum = 0;
                for (int i=indexes.first; i<indexes.second; ++i) {
                        local_sum += arr[i];
                }
                shared_total_sum += local_sum;
        };

        DLOG("multi_threaded_sum_v2", "chunk_work_size=%d", chunk_work_size);
        DLOG("multi_threaded_sum_v2", "num_max_threads=%d", num_max_threads);

        std::vector<std::thread> threads;
        threads.reserve(num_max_threads);

        for (int i=0; i<num_max_threads; ++i) {
                int start = i * chunk_work_size;
                // also check if there's remaining to piggyback works into the last chunk
                int end = (i == num_max_threads-1) && (start + chunk_work_size < num_elem-1) ? num_elem : start+chunk_work_size;
                threads.emplace_back(worker_func, arr, std::make_pair(start, end));
        }

        DLOG("multi_threaded_sum_v2", "thread_size=%d", threads.size());

        for (auto& th : threads) {
                th.join();
        }

        std::chrono::duration<double, std::milli> exec_time = std::chrono::steady_clock::now() - start;
        std::cout << "elapsed time: " << exec_time.count() << "ms" << std::endl;

        return shared_total_sum;
}

以上是完整的代码,但为了便于理解,我们单独剖析每个部分。 下面将详细解释每个代码模块。

它们都可在 pthread 和 win32 线程之间切换。

#include "example.h"

#ifdef USE_MINGW_STD_THREAD
        #include <mingw-std-threads/mingw.thread.h>
#else
        #include <thread>
#endif

依据此设置,我们可以与我们的构建系统很好地集成,以便在 pthread 或 win32 线程和链接之间切换。 在编译标志中添加 -DUSE_MINGW_STD_THREAD,以便在我们进行交叉编译时使用 win32 线程。

实现简单的接口,直截了当。

EXAMPLE_API int add(int a, int b) noexcept {
        return a + b;
}

EXAMPLE_API int sub(int a, int b) noexcept {
        return a - b;
}

EXAMPLE_API int num_hardware_concurrency() noexcept {
        return std::thread::hardware_concurrency();
}

add() 和 sub() 简单易懂。 对于 num_hardware_concurrency(),我们需要包含头文件 <thread> 以便调用 std::thread::hardware_concurrency()

调式日志实用程序函数。

#ifdef ENABLE_DEBUG
#include <cstdarg>
#endif

#ifdef ENABLE_DEBUG
const int LOG_BUFFER_SIZE = 2048;
char log_buffer[LOG_BUFFER_SIZE];

inline void DLOG(const char* ctx, const char* format, ...) {
        va_list args;
        va_start(args, format);
        std::vsnprintf(log_buffer, LOG_BUFFER_SIZE-1, format, args);
        va_end(args);

        std::cout << "[DEBUG] [" << ctx << "] " << log_buffer << std::endl;
}
#else
        #define DLOG(...)
#endif

通过在编译标志中添加 -DENABLE_DEBUG<,我们可以将调试日志打印到控制台上。 这就是为什么我建议通过命令行启动 MetaTrader 5,这样我们就可以相应地调试我们的程序。 若我们没有这样定义时,DLOG() 没有任何意义,它不会对我们的代码产生任何影响,无论是在执行速度方面,还是在共享库、或可执行的二进制文件大小方面。 这很不错。

DLOG() 的设计灵感来自 Android 开发,因为通常会有一个上下文字符串(来自任何组件调试日志),在这种情况下是 ctx,然后是调试日志字符串。

单线程求和函数的实现。

EXAMPLE_API int single_threaded_sum(const int arr[], int num_elem) {
        auto start = std::chrono::steady_clock::now();

        int local_sum = 0;
        for (int i=0; i<num_elem; ++i) {
                local_sum += arr[i];
        }

        std::chrono::duration<double, std::milli> exec_time = std::chrono::steady_clock::now() - start;
        std::cout << "elapsed time: " << exec_time.count() << "ms" << std::endl;
        return local_sum;
}

它模拟使用 MQL5 代码的实际用法。 想象一下这样的情形,MQL5 将一些数据作为数组发送给 DLL 函数,目的是让 DLL 计算一些东西,并将结果返回给之前的 MQL5 代码。
就是如此。 但是对于此函数,它会线性迭代指定输入数组中的所有元素,按 num_elem 指定的总数逐一获取元素。

该代码还通过使用 std::chrono 库来计算运行时间,从而对执行的总时间进行基准测试。 请注意,我们所用的 std::chrono::steady_clock 是一个单调时钟,可不受系统时钟调整影响的情况下一直向前移动。 它完全适用于测量时间间隔。

多线程求和函数的实现。

EXAMPLE_API int multi_threaded_sum_v2(const int arr[], int num_elem) {
        auto start = std::chrono::steady_clock::now();

        std::vector<std::pair<int, int>> arr_indexes;

        const int num_max_threads = std::thread::hardware_concurrency() == 0 ? 2 : std::thread::hardware_concurrency();
        const int chunk_work_size = num_elem / num_max_threads;

        std::atomic<int> shared_total_sum(0);

        // a lambda function that accepts input vector by reference
        auto worker_func = [&shared_total_sum](const int arr[], std::pair<int, int> indexes) {
                int local_sum = 0;
                for (int i=indexes.first; i<indexes.second; ++i) {
                        local_sum += arr[i];
                }
                shared_total_sum += local_sum;
        };

        DLOG("multi_threaded_sum_v2", "chunk_work_size=%d", chunk_work_size);
        DLOG("multi_threaded_sum_v2", "num_max_threads=%d", num_max_threads);

        std::vector<std::thread> threads;
        threads.reserve(num_max_threads);

        for (int i=0; i<num_max_threads; ++i) {
                int start = i * chunk_work_size;
                // also check if there's remaining to piggyback works into the last chunk
                int end = (i == num_max_threads-1) && (start + chunk_work_size < num_elem-1) ? num_elem : start+chunk_work_size;
                threads.emplace_back(worker_func, arr, std::make_pair(start, end));
        }

        DLOG("multi_threaded_sum_v2", "thread_size=%d", threads.size());

        for (auto& th : threads) {
                th.join();
        }

        std::chrono::duration<double, std::milli> exec_time = std::chrono::steady_clock::now() - start;
        std::cout << "elapsed time: " << exec_time.count() << "ms" << std::endl;

        return shared_total_sum;
}

请注意,它被标记为 v2,是出于历史原因,我将其保留。 简而言之,对于采用 MESI 缓存一致性协议的现代处理器,没有必要将数据集的副本馈送到每个线程当中,因为 MESI 会将这样的缓存行打上标记,并在多个线程之间共享,且不会浪费任何 CPU 周期来发送信号,及等待响应。 我在之前的 v1 实现里需全力以赴地制作数据集的副本,并将其馈送到每个线程之中。 但是根据所提到的现代处理器已经使用 MESI 的原因,现在已无必要在源代码中包含这种尝试。 v1 比 v2 至少慢 2-5 倍。

请注意 worker_func 这是一个 lambda 函数,它处理原始数据数组,并操控所用数据范围(开始和结束索引对)。 它在循环中的把所有元素汇总到一个局部变量之中,以避免假共享问题,该问题可能会显著降低性能,然后最终累加所有原子方式的线程到共享合计变量。 它使用 std::atomic 来助力线程安全。 此种类共享合计变量的倍数需要修改到足够少,才不会对性能产生重大影响。 平衡实现的实施和速度增益是一条要走的途径。

我们计算拆分工作需要多少线程,因此稍后就会知道每个线程的工作范围。 请注意,std::hardware_concurrency() 可以返回 0,这意味着它可能无法确定线程数量,因此我们也需处理这种情况,并回退到 2。

接下来,我们创建一个线程向量。 保留其容量 num_max_threads 。然后迭代计算要处理的每个线程的数据集范围。 请注意,对于最后一个线程,它将获取所有剩余数据,因为全部要完成处理的元素数量可能不会被计算线程数量整除。

重要的是,我们加入了所有线程。 对于更复杂的情况,我们也许需要异步环境,这样就不会被 MQL5 代码等待结果阻塞。 据此,我们通常调用 std::future,它是所有 std::async、std:::promise 和 std::packaged_task 的基础。因此,我们通常至少有 2 个接口,一个用于发出请求,将数据从 MQL5 代码发送到 DLL 计算,而不会阻塞;另一个按需接收此类请求的结果,此时,它将阻塞 MQL5 代码的调用。 我也许会在以后的文章中写点有关内容。

此外,在此过程中,我们可以使用 DLOG() 打印一些调试状态。 这对调试很有帮助。

接下来,我们实现在本机 Linux 上运行的可移植主测试程序,并通过 Wine 交叉编译环境。

main.cpp

#include "example.h"
#include <iostream>
#include <cassert>
#include <vector>
#include <memory>

int main() {
        int res = 0;

        std::cout << "--- misc ---\n";
        res = add(1,2);
        std::cout << "add(1,2): " << res << std::endl;
        assert(res == 3);

	res = 0;

        res = sub(2,1);
        std::cout << "sub(2,1): " << res << std::endl;
        assert(res == 1);

	res = 0;

        std::cout << "hardware concurrency: " << num_hardware_concurrency() << std::endl;
        std::cout << "--- end ---\n" << std::endl;

        std::vector<int> arr(1000000000, 1);

        std::cout << "--- single-threaded sum(1000M) ---\n";
        res = single_threaded_sum(arr.data(), arr.size());
        std::cout << "sum: " << res << std::endl;
        assert(res == 1000000000);
        std::cout << "--- end ---\n" << std::endl;
        
        res = 0;

        std::cout << "--- multi-threaded sum_v2(1000M) ---\n";
        res = multi_threaded_sum_v2(arr.data(), arr.size());
        std::cout << "sum: " << res << std::endl;
        assert(res == 1000000000);
        std::cout << "--- end ---" << std::endl;

        return 0;
}

正常情况下,我们要包含 example.h 头文件,以便能够调用这些已实现的接口。 我们还调用 assert() 验证结果是否正确。

接下来,我们构建共享库(作为 Linux 本机的 libexample.so 库),和主测试程序 main.out。 不是首先使用构建系统,而是通过命令行执行。 稍后我们将通过 Makefile 实现正确的构建系统。
在进行交叉编译之前,我们首先在 Linux 本机上测试它。

执行以下命令以构建要作为 libexample.so 输出的共享库。

$ g++ -shared -std=c++17 -Wall -Wextra -fno-rtti -O2 -I. -fPIC -o libexample.so example.cpp -lpthread

每个标志的说明如下

标志说明
-shared指令编译器构建共享库
-std=c++17指令编译器基于 C++17 标准的 C++ 语法
-Wall指令编译时输出所有警告
-Wextra指令在编译时输出额外的警告
-fno-rtti这是优化的一部分。 它禁用 RTTI(运行时类型信息)。
RTTI 允许在程序执行期间判定我们不需要的对象类型,因为它会产生性能成本。
-O2启用优化级别 2,其中包括在级别 1 之上进行更激进的优化
-I.将包含路径设置为当前目录,因此编译器将能够找到位于同一目录的头文件 example.h
-fPIC通常,每次构建共享库时需要,因为它指令编译器为将要创建的共享库生成相应的位置无关代码(PIC)
,并与要链接的主程序一起工作。 由于没有固定的内存地址来从共享库加载特定功能,它也提高了安全性。
-lpthread 指令与 pthread 库链接

执行以下命令构建一个与 libexample.so 链接的主测试程序,并以 main.out 输出。

$ g++ -std=c++17 -Wall -Wextra -fno-rtti -O2 -I. -o main.out main.cpp -L. -lexample

与我们上面未提到的不同标志,解释如下

标志说明
-L.将共享库的包含路径设置为位于同一目录中
 -lexample共享库的链接,即 libexample.so

最后,我们执行可执行文件。

$ ./main.out 
--- misc ---
add(1,2): 3
sub(2,1): 1
hardware concurrency: 12
--- end ---

--- single-threaded sum(1000M) ---
elapsed time: 568.401ms
sum: 1000000000
--- end ---

--- multi-threaded sum_v2(1000M) ---
elapsed time: 131.697ms
sum: 1000000000
--- end ---
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值