Appearance
客户端终端全局变量
在上一章中,我们学习了与文件相关的MQL5函数。这些函数为读写任意数据提供了广泛且灵活的选择。然而,有时一个MQL程序需要一种更简便的方法,以便在程序运行的不同阶段之间保存和恢复某个属性的状态。
例如,我们想要计算某些统计数据:程序被启动了多少次,有多少个实例在不同的图表上并行执行等等。在程序自身内部是无法累积这些信息的。必须要有某种外部的长期存储方式。虽然创建一个文件来实现这一点是可行的,但代价会比较高。
许多程序被设计为相互交互,也就是说,它们必须以某种方式交换信息。如果是与终端外部的程序集成,或者要传输大量数据,那么不使用文件确实很难做到。然而,当要发送的数据量不大,并且所有程序都是用MQL5编写并在MetaTrader 5内部运行时,使用文件似乎就显得多余了。对于这种情况,终端提供了一种更简单的技术:全局变量。
全局变量是终端共享内存中的一个命名存储位置。任何MQL程序都可以创建、修改或删除它,但它并不专属于某个程序,并且对所有其他MQL程序都是可用的。全局变量的名称是任何唯一的(在所有变量中)、不超过63个字符的字符串。这个字符串不必满足MQL5中对变量标识符的要求,因为终端的全局变量并不是通常意义上的变量。程序员不会像我们在“变量”章节中所学的那样在源代码中定义它们,它们也不是MQL程序的一个组成部分,对它们的任何操作都只能通过调用我们将在本章中描述的特殊函数之一来完成。
全局变量只允许存储双精度型(double)的值。如果有必要,你可以将其他类型的值打包或转换为双精度型,或者使用变量名的一部分(例如,遵循某个特定的前缀)来存储字符串。
在终端运行期间,全局变量存储在随机存取存储器(RAM)中,并且几乎可以立即访问:唯一的开销与函数调用相关。这无疑使全局变量相对于使用文件具有优势,因为在处理文件时,获取文件句柄是一个相对较慢的过程,并且文件句柄本身会消耗一些额外的资源。
在终端会话结束时,全局变量会被卸载到一个特殊文件(gvariables.dat)中,然后在下次启动终端时从该文件中恢复。
如果某个特定的全局变量在4周内未被使用,终端会自动销毁它。这种行为依赖于对变量最后使用时间的跟踪和存储,这里的“使用”是指设置一个新值或读取一个旧值(但不包括检查变量是否存在或获取最后使用时间)。
请注意,全局变量与账户、配置文件或交易环境的任何其他特征都没有关联。因此,如果它们要存储与交易环境相关的信息(例如,某个特定账户的一些通用限制),在构造变量名时应考虑到所有影响算法和决策的因素。为了区分同一智能交易系统(EA)的多个实例的全局变量,你可能需要在变量名中添加工作品种、时间框架或EA设置中的“魔术数字”。
除了MQL程序之外,用户也可以手动创建全局变量。现有全局变量的列表以及对它们进行交互式管理的工具,可以在终端中通过“工具” -> “全局变量”(F3)命令打开的对话框中找到。
在这里,通过相应的按钮你可以添加和删除全局变量,双击“变量”或“值”列可以编辑特定变量的名称或值。以下是可用的键盘快捷键:F2用于编辑名称,F3用于编辑值,Ins用于添加新变量,Del用于删除所选变量。
稍后,我们将学习两种主要类型的MQL程序——智能交易系统和指标。它们的特殊之处在于能够在测试器中运行,并且在测试器中全局变量的相关函数也能正常工作。然而,在测试器中,全局变量是由测试器代理创建、存储和管理的。换句话说,在测试器中无法访问终端的全局变量列表,并且由被测试程序创建的那些变量属于特定的代理,它们的生命周期仅限于一次测试过程。也就是说,某个代理的全局变量对其他代理是不可见的,并且会在测试运行结束时被删除。特别是,如果智能交易系统在多个代理上进行优化,它可以操作全局变量,以便在同一代理的上下文中与它所使用的指标“通信”,因为它们在同一个代理中一起执行,但在并行的代理上,智能交易系统的其他副本将形成它们自己的变量列表。
使用全局变量在MQL程序之间进行数据交换并不是唯一可用的方式,也不总是最合适的方式。特别是,智能交易系统和指标是交互式的MQL程序类型,它们可以在图表上生成和接受事件。你可以在事件参数中传递各种类型的信息。此外,可以准备计算数据数组,并以指标缓冲区的形式提供给其他MQL程序。位于图表上的MQL程序可以使用用户界面图形对象来传输和存储信息。
从技术角度来看,全局变量的最大数量仅受操作系统资源的限制。然而,对于大量的元素,建议使用更合适的方式:文件或数据库。
全局变量的写入和读取
MQL5应用程序编程接口(API)提供了两个用于写入和读取全局变量的函数:GlobalVariableSet
和 GlobalVariableGet
(有两个版本)。
datetime GlobalVariableSet(const string name, double value)
该函数为名为 name
的全局变量设置一个新值。如果在调用该函数之前该变量不存在,它将被创建。如果该变量已经存在,之前的值将被 value
替换。
如果操作成功,该函数将返回变量的修改时间(计算机的当前本地时间)。如果出现错误,返回值为0。
double GlobalVariableGet(const string name)
bool GlobalVariableGet(const string name, double &value)
该函数返回名为 name
的全局变量的值。调用第一个版本函数的结果,在成功时仅包含变量的值,在出错时为0。由于变量可能包含值0(这与错误指示相同),所以如果接收到的值为0,就需要解析内部错误代码 _LastError
,以区分是正常的0值还是错误导致的0值。特别是,如果尝试读取一个不存在的变量,将生成内部错误4501(GLOBALVARIABLE_NOT_FOUND
,即全局变量未找到)。
在某些算法中,当获取到0值可以作为之前不存在变量的默认初始化值时,这个函数版本使用起来很方便(见下面的示例)。如果需要以特殊方式处理变量不存在的情况(特别是要计算其他一些起始值),则应该首先使用 GlobalVariableCheck
函数检查变量是否存在,并根据检查结果执行不同的代码分支。或者,也可以使用第二个版本的函数。
第二个版本的函数根据执行是否成功返回 true
或 false
。如果成功,终端全局变量的值将被存储在作为第二个参数以引用方式传递的接收变量 value
中。如果变量不存在,则返回 false
。
在测试脚本 GlobalsRunCount.mq5
中,我们使用一个全局变量来统计它运行的次数。该变量的名称就是源文件的名称。
const string gv = __FILE__;
回想一下,内置宏 __FILE__
(见预定义常量)会被编译器展开为已编译文件的名称,也就是说,在这种情况下是 "GlobalsRunCount.mq5"
。
在 OnStart
函数中,我们将尝试读取给定的全局变量,并将结果保存到局部变量 count
中。如果之前没有这个全局变量,我们会得到0,这对我们来说是可以的(我们从0开始计数)。
在将值保存到 count
之前,有必要将其强制转换为 (int)
类型,因为 GlobalVariableGet
函数返回的是 double
类型,如果不进行转换,编译器会生成关于可能的数据丢失的警告(它不知道我们只打算存储整数)。
void OnStart()
{
int count = (int)PRTF(GlobalVariableGet(gv));
count++;
PRTF(GlobalVariableSet(gv, count));
Print("This script run count: ", count);
}
然后我们将计数器加1,并使用 GlobalVariableSet
函数将其写回全局变量。如果我们多次运行这个脚本,将会得到类似以下的日志记录(你的时间戳会不同):
GlobalVariableGet(gv)=0.0 / GLOBALVARIABLE_NOT_FOUND(4501)
GlobalVariableSet(gv,count)=2021.08.29 16:04:40 / ok
This script run count: 1
GlobalVariableGet(gv)=1.0 / ok
GlobalVariableSet(gv,count)=2021.08.29 16:05:00 / ok
This script run count: 2
GlobalVariableGet(gv)=2.0 / ok
GlobalVariableSet(gv,count)=2021.08.29 16:05:21 / ok
This script run count: 3
需要注意的是,在第一次运行时,我们得到的值为0,并且生成了内部错误标志4501。所有后续的调用都能顺利执行,因为变量已经存在了(可以在终端的“全局变量”窗口中看到)。有兴趣的人可以关闭终端,重新启动它,然后再次执行这个脚本:计数器将从之前的值继续增加。
检查全局变量的存在性和最后活动时间
正如我们在上一节中所看到的,你可以通过尝试读取全局变量的值来检查它是否存在:如果_LastError
中没有产生错误代码,那么该全局变量存在,并且我们已经获取到了它的值,可以在算法中使用。然而,在某些情况下,你可能只需要检查变量是否存在,而不需要读取它的值,这时使用专门为此设计的另一个函数GlobalVariableCheck
会更加方便。
还有另一种检查方式,即使用GlobalVariableTime
函数。顾名思义,这个函数可以让你了解变量最后一次被使用的时间。但如果变量不存在,那么它的使用时间也就不存在,即等于0。
bool GlobalVariableCheck(const string name)
该函数用于检查指定名称的全局变量是否存在,并返回检查结果:true
(变量存在)或false
(变量不存在)。
datetime GlobalVariableTime(const string name)
该函数返回指定名称的全局变量最后一次被使用的时间。这里的“使用”可以是对变量值的修改或读取。
使用GlobalVariableCheck
检查变量是否存在,或者通过GlobalVariableTime
获取变量的使用时间,这些操作不会改变变量的使用时间。
在脚本GlobalsRunCheck.mq5
中,我们将对GlobalsRunCount.mq5
中的代码进行一些补充,以便在OnStart
函数的最开始检查变量是否存在以及它的使用时间。
void OnStart()
{
PRTF(GlobalVariableCheck(gv));
PRTF(GlobalVariableTime(gv));
...
下面的代码保持不变。需要注意的是,通过__FILE__
定义的gv
变量,这次将包含新脚本的名称"GlobalsRunCheck.mq5"
作为全局变量的名称(也就是说,每个脚本都有自己的全局计数器)。
除了第一次运行之外,所有后续运行中,GlobalVariableCheck
函数都会返回true
(变量存在),并且GlobalVariableTime
函数会返回上一次运行时该变量的使用时间。以下是一个示例日志:
GlobalVariableCheck(gv)=false / ok
GlobalVariableTime(gv)=1970.01.01 00:00:00 / GLOBALVARIABLE_NOT_FOUND(4501)
GlobalVariableGet(gv)=0.0 / GLOBALVARIABLE_NOT_FOUND(4501)
GlobalVariableSet(gv,count)=2021.08.29 16:59:35 / ok
This script run count: 1
GlobalVariableCheck(gv)=true / ok
GlobalVariableTime(gv)=2021.08.29 16:59:35 / ok
GlobalVariableGet(gv)=1.0 / ok
GlobalVariableSet(gv,count)=2021.08.29 16:59:45 / ok
This script run count: 2
GlobalVariableCheck(gv)=true / ok
GlobalVariableTime(gv)=2021.08.29 16:59:45 / ok
GlobalVariableGet(gv)=2.0 / ok
GlobalVariableSet(gv,count)=2021.08.29 16:59:56 / ok
This script run count: 3
获取全局变量列表
很多时候,MQL程序需要遍历现有的全局变量,并筛选出符合某些条件的变量。例如,如果一个程序使用变量名的一部分来存储文本信息,那么可能事先只知道变量名的前缀。这个前缀的作用是识别“自己的”变量,而附加在该前缀后面的“有效负载”内容使得无法通过精确的名称来查找变量。
MQL5 API提供了两个函数来枚举全局变量。
int GlobalVariablesTotal()
该函数返回全局变量的总数。
string GlobalVariableName(int index)
该函数根据全局变量在列表中的索引编号返回其名称。请求变量的索引参数 index
必须在从0到 GlobalVariablesTotal() - 1
的范围内。
如果出现错误,该函数将返回 NULL
,并且可以从服务变量 _LastError
或 GetLastError
函数中获取错误代码。
让我们使用脚本 GlobalsList.mq5
来测试这两个函数。
void OnStart()
{
PRTF(GlobalVariableName(1000000));
int n = PRTF(GlobalVariablesTotal());
for(int i = 0; i < n; ++i)
{
const string name = GlobalVariableName(i);
PrintFormat("%d %s=%f", i, name, GlobalVariableGet(name));
}
}
第一行代码故意请求一个编号很大的变量的名称,这个变量很可能不存在,这会引发一个错误。接下来,请求实际的变量数量,然后遍历所有变量,并输出它们的名称和值。下面的日志包含了之前测试脚本创建的变量以及一个第三方变量。
GlobalVariableName(1000000)= / GLOBALVARIABLE_NOT_FOUND(4501)
GlobalVariablesTotal()=3 / ok
0 GlobalsRunCheck.mq5=3.000000
1 GlobalsRunCount.mq5=4.000000
2 abracadabra=0.000000
终端按索引返回变量的顺序是未定义的。
删除全局变量
如果有必要,MQL程序可以删除一个或一组不再需要的全局变量。全局变量列表会消耗一些计算机资源,良好的编程风格建议在可能的情况下释放这些资源。
bool GlobalVariableDel(const string name)
该函数删除名为 name
的全局变量。如果操作成功,函数返回 true
,否则返回 false
。
int GlobalVariablesDeleteAll(const string prefix = NULL, datetime limit = 0)
该函数删除名称中具有指定前缀且使用时间早于 limit
参数值的全局变量。
如果指定了 NULL
前缀(默认值)或空字符串,那么所有同时符合按日期删除标准(如果设置了该标准)的全局变量都将符合删除条件。
如果 limit
参数为零(默认值),则删除考虑前缀后任何日期的全局变量。
如果同时指定了这两个参数,那么既符合前缀标准又符合时间标准的全局变量将被删除。
请注意:不带参数调用 GlobalVariablesDeleteAll
将删除所有变量。
该函数返回已删除变量的数量。
考虑使用脚本 GlobalsDelete.mq5
,它利用了两个新函数特性。
void OnStart()
{
PRTF(GlobalVariableDel("#123%"));
PRTF(GlobalVariablesDeleteAll("#123%"));
...
首先,尝试通过精确名称和前缀删除不存在的全局变量。这两种操作对现有变量都没有影响。
使用按过去时间(超过4周前)过滤的方式调用 GlobalVariablesDeleteAll
也会得到结果为零,因为终端会自动删除这么旧的变量(这样的变量不可能存在)。
PRTF(GlobalVariablesDeleteAll(NULL, D'2021.01.01'));
然后,我们创建一个名为 “abracadabra” 的变量(如果它不存在),并立即删除它。这些调用应该会成功。
PRTF(GlobalVariableSet(abracadabra, 0));
PRTF(GlobalVariableDel(abracadabra));
最后,让我们删除以 “GlobalsRun” 为前缀的变量:这些变量应该是由前两节关于文件名的测试脚本(分别是 “GlobalsRunCount.mq5” 和 “GlobalsRunCheck.mq5”)创建的。
PRTF(GlobalVariablesDeleteAll("GlobalsRun"));
PRTF(GlobalVariablesTotal());
}
该脚本应该会在日志中输出类似以下的一组字符串(一些指标取决于外部条件和启动时间)。
GlobalVariableDel(#123%)=false / GLOBALVARIABLE_NOT_FOUND(4501)
GlobalVariablesDeleteAll(#123%)=0 / ok
GlobalVariablesDeleteAll(NULL,D'2021.01.01')=0 / ok
GlobalVariableSet(abracadabra,0)=2021.08.30 14:02:32 / ok
GlobalVariableDel(abracadabra)=true / ok
GlobalVariablesDeleteAll(GlobalsRun)=2 / ok
GlobalVariablesTotal()=0 / ok
最后,我们打印出剩余全局变量的总数(在这种情况下,我们得到 0,即没有变量了)。如果全局变量是由其他 MQL 程序或用户创建的,你得到的结果可能会有所不同。
临时全局变量
在终端的全局变量子系统中,可以将某些变量设置为临时变量:它们仅存储在内存中,并且在关闭终端时不会写入磁盘。
由于其特殊性质,临时全局变量专门用于MQL程序之间的数据交换,不适用于在MetaTrader 5启动之间保存状态。临时变量最明显的用途之一是各种操作活动的度量指标(例如,正在运行的程序副本的计数器),这些指标应该在每次启动时动态重新计算,而不是从磁盘恢复。
在为全局变量赋值之前,应该提前使用GlobalVariableTemp
函数将其声明为临时变量。
遗憾的是,无法通过全局变量的名称来确定它是否为临时变量:MQL5没有提供这样的方法。
临时变量只能通过MQL程序创建。临时变量会与普通(持久)全局变量一起显示在“全局变量”窗口中,但用户无法通过图形用户界面(GUI)添加自己的临时变量。
bool GlobalVariableTemp(const string name)
该函数创建一个具有指定名称的新全局变量,该变量仅在当前终端会话结束之前存在。
如果已经存在具有相同名称的变量,它不会被转换为临时变量。
然而,如果该变量尚不存在,它将被赋予值0。此后,你可以像平常一样使用它,特别是可以使用GlobalVariableSet
函数为其赋值。
我们将结合下一节的函数展示此函数的一个示例。
使用全局变量同步程序
由于全局变量存在于MQL程序之外,它们对于组织控制同一程序多个副本的外部标志,或在不同程序之间传递信号非常有用。最简单的例子是限制可运行的程序副本数量。这可能是必要的,以防止在不同图表上意外复制专家顾问(因为这可能会使交易订单翻倍),或者实现一个演示版本。
乍一看,这样的检查可以在源代码中按如下方式进行。
c
void OnStart()
{
const string gv = "AlreadyRunning";
// 如果变量存在,则已有一个实例在运行
if(GlobalVariableCheck(gv)) return;
// 创建一个变量作为标志,表明有一个正在运行的副本
GlobalVariableSet(gv, 0);
while(!IsStopped())
{
// 工作循环
}
// 在退出前删除变量
GlobalVariableDel(gv);
}
这里以一个脚本为例展示了最简单的版本。对于其他类型的MQL程序,检查的总体概念是相同的,尽管指令的位置可能不同:专家顾问和指标不是使用无限的工作循环,而是使用它们特有的事件处理程序,这些处理程序会被终端反复调用。我们稍后将研究这些问题。
上述代码的问题在于,它没有考虑到MQL程序的并行执行。
一个MQL程序通常在它自己的线程中运行。在四种MQL程序类型中的三种,即专家顾问、脚本和服务,系统肯定会分配单独的线程。至于指标,对于在相同的交易品种和时间框架组合上工作的所有指标实例,系统会分配一个公共线程。但是,不同组合上的指标仍然属于不同的线程。
几乎总是有很多线程在终端中运行——远远超过处理器核心的数量。因此,每个线程会不时地被系统暂停,以便让其他线程工作。由于所有这些线程之间的切换都非常快,作为用户的我们不会注意到这种 “内部组织”。然而,每次暂停都可能影响不同线程访问共享资源的顺序。全局变量就是这样的共享资源。
从程序的角度来看,在任何相邻的指令之间都可能发生暂停。如果知道这一点,我们再看一下我们的例子,不难发现一个地方,在那里使用全局变量的逻辑可能会被破坏。
确实,第一个副本(线程)可以进行检查,发现没有变量,但会立即被暂停。结果,在它有时间用下一条指令创建变量之前,执行上下文切换到了第二个副本。第二个副本也不会找到该变量,并会像第一个副本一样决定继续工作。为了更清楚地说明,两个副本的相同源代码如下所示,作为两列指令,按照它们交错执行的顺序排列。
Copy 1 | Copy 2 |
---|---|
```c | |
void OnStart() | |
{ | |
const string gv = "AlreadyRunning"; |
if(GlobalVariableCheck(gv)) return; // 没有变量
GlobalVariableSet(gv, 0);
// "我是第一个且唯一的" while(!IsStopped())
{
;
}
GlobalVariableDel(gv);
}
c
void OnStart()
{
const string gv = "AlreadyRunning";
if(GlobalVariableCheck(gv)) return;
// 仍然没有变量
GlobalVariableSet(gv, 0);
// "不,我是第一个且唯一的"
while(!IsStopped())
{
;
}
GlobalVariableDel(gv);
}
当然,这种线程之间的切换方案有一定的约定性。但在这种情况下,即使是在一行代码中,程序逻辑被违反的可能性也是很重要的。当有许多程序(线程)时,对共享资源进行意外操作的概率就会增加。这可能足以在最意想不到的时刻让专家顾问产生亏损,或者得到扭曲的技术分析估计。
这类错误最令人沮丧的是,它们很难被检测到。编译器无法检测到它们,而且它们在运行时会偶尔出现。但是,如果错误长时间没有暴露出来,这并不意味着没有错误。
为了解决这样的问题,有必要以某种方式同步所有程序副本对共享资源(在这种情况下是全局变量)的访问。
在计算机科学中,有一个特殊的概念——互斥锁(mutex,mutual exclusion),它是一个用于为并行程序提供对共享资源的独占访问的对象。互斥锁可以防止由于异步更改而导致数据丢失或损坏。通常,访问互斥锁会同步不同的程序,因为在特定时刻,只有其中一个程序可以通过获取互斥锁来编辑受保护的数据,而其他程序则被迫等待,直到互斥锁被释放。
在MQL5中没有纯粹形式的现成互斥锁。但是对于全局变量,可以通过以下函数获得类似的效果,我们将对其进行研究。
c
bool GlobalVariableSetOnCondition(const string name, double value, double precondition)
该函数在现有全局变量 name
的当前值等于 precondition
的情况下,设置其新值为 value
。
如果成功,该函数返回 true
。否则,它返回 false
,并且错误代码将存储在 _LastError
中。特别是,如果变量不存在,该函数将生成 ERR_GLOBALVARIABLE_NOT_FOUND (4501)
错误。
该函数提供对全局变量的原子访问,也就是说,它以不可分割的方式执行两个操作:检查其当前值,如果与条件匹配,则为变量分配一个新值。
等效的函数代码大致可以表示如下(为什么是 “大致” 的,我们稍后会解释):
c
bool GlobalVariableZetOnCondition(const string name, double value, double precondition)
{
bool result = false;
{ /* 启用中断保护 */ }
if(GlobalVariableCheck(name) && (GlobalVariableGet(name) == precondition))
{
GlobalVariableSet(name, value);
result = true;
}
{ /* 禁用中断保护 */ }
return result;
}
由于两个原因,无法实现像这样按预期工作的代码。首先,在纯MQL5中没有办法实现启用和禁用中断保护的代码块(在内置的 GlobalVariableSetOnCondition
函数中,这是由内核本身提供的)。其次,GlobalVariableGet
函数调用会更改变量的最后使用时间,而 GlobalVariableSetOnCondition
函数如果条件不满足则不会更改它。
为了演示如何使用 GlobalVariableSetOnCondition
,我们将转向一种新的MQL程序类型:服务。我们将在单独的部分中详细研究它们。目前,应该注意的是,它们的结构与脚本非常相似:对于两者来说,都只有一个主函数(入口点)OnStart
。唯一的重要区别是,脚本在图表上运行,而服务自行运行(在后台)。
用服务替换脚本的必要性在于,我们使用 GlobalVariableSetOnCondition
的任务的应用意义在于计算正在运行的程序实例的数量,并有可能设置一个限制。在这种情况下,只有在启动多个程序的时刻才会发生与同时修改共享计数器相关的冲突。然而,对于脚本来说,在相对较短的时间内在不同图表上运行多个副本是相当困难的。对于服务来说,相反,终端界面有一个方便的机制用于批量(分组)启动。此外,所有激活的服务将在终端下次启动时自动启动。
所提出的计算副本数量的机制对于其他类型的MQL程序也将是有用的。由于专家顾问和指标即使在终端关闭时也会保持附着在图表上,因此下次打开终端时,所有程序几乎会同时读取它们的设置和共享资源。因此,如果某些专家顾问和指标内置了副本数量限制,基于全局变量同步计数是至关重要的。
首先,让我们考虑一个以简单模式实现副本控制的服务,不使用 GlobalVariableSetOnCondition
,并确保计数器故障的问题是真实存在的。服务位于通用源代码目录中的一个专用子目录中,所以完整路径是 —— MQL5/Services/MQL5Book/p4/GlobalsNoCondition.mq5
。
在服务文件的开头应该有一个指令:
c
#property service
在服务中,我们将提供两个输入变量,用于设置允许并行运行的副本数量限制,以及一个延迟时间,用于模拟由于计算机磁盘和CPU的大量负载而导致的执行中断,这在终端启动时经常发生。这将使我们更容易重现问题,而无需多次重启终端来期望出现不同步的情况。所以,我们要捕捉一个可能只是偶尔发生,但一旦发生就会带来严重后果的错误。
c
input int limit = 1; // 限制
input int startPause = 100;// 延迟(毫秒)
延迟模拟基于 Sleep
函数。
c
void Delay()
{
if(startPause > 0)
{
Sleep(startPause);
}
}
首先,在 OnStart
函数内部声明一个临时全局变量。由于它是用于计算正在运行的程序副本数量的,所以没有必要将其设为常量:每次启动终端时都需要重新计数。
c
void OnStart()
{
PRTF(GlobalVariableTemp(__FILE__));
...
为了避免用户事先创建同名变量并为其分配负值的情况,我们引入保护措施。
c
int count = (int)GlobalVariableGet(__FILE__);
if(count < 0)
{
Print("Negative count detected. Not allowed.");
return;
}
接下来,主要功能片段开始。如果计数器已经大于或等于最大允许数量,我们中断程序启动。
c
if(count >= limit)
{
PrintFormat("Can't start more than %d copy(s)", limit);
return;
}
否则,我们将计数器增加1并将其写入全局变量。事先,我们模拟延迟,以便引发一种情况,即在我们的程序读取变量和写入变量之间,另一个程序可能会介入。
c
Delay();
PRTF(GlobalVariableSet(__FILE__, count + 1));
如果确实发生这种情况,我们的程序副本将递增并分配一个已经过时、不正确的值。这将导致一种情况,即在与我们的程序并行运行的另一个程序副本中,相同的计数值已经被处理或将会再次被处理。
服务的有用工作由以下循环表示。
c
int loop = 0;
while(!IsStopped())
{
PrintFormat("Copy %d is working [%d]...", count, loop++);
// ...
Sleep(3000);
}
在用户停止服务后(为此,界面有一个上下文菜单;稍后会详细介绍),循环将结束,我们需要将计数器减1。
c
int last = (int)GlobalVariableGet(__FILE__);
if(last > 0)
{
PrintFormat("Copy %d (out of %d) is stopping", count, last);
Delay();
PRTF(GlobalVariableSet(__FILE__, last - 1));
}
else
{
Print("Count underflow");
}
}
编译后的服务会进入 “导航器” 中的相应分支。
“导航器” 中的服务和上下文菜单
通过右键单击,我们将打开上下文菜单,并通过两次调用 “添加服务” 命令来创建 GlobalsNoCondition.mq5
服务的两个实例。在这种情况下,每次都会打开一个带有服务设置的对话框,在其中应保留参数的默认值。
重要的是要注意,“添加服务” 命令会立即启动创建的服务。但我们不需要这样。因此,在启动每个副本后,我们必须再次调用上下文菜单并执行 “停止” 命令(如果选择了特定实例),或者 “全部停止”(如果选择了程序,即生成的整个实例组)。
服务的第一个实例默认名称将与服务文件完全匹配(“GlobalsNoCondition”),在所有后续实例中,会自动添加一个递增的数字。特别是,第二个实例显示为 “GlobalsNoCondition 1”。终端允许使用 “重命名” 命令将实例重命名为任意文本,但我们不会这样做。
现在一切都准备好进行实验了。让我们尝试同时运行两个实例。为此,让我们对相应的 GlobalsNoCondition
分支运行 “全部运行” 命令。
让我们提醒一下,在参数中设置了1个实例的限制。然而,根据日志,这没有起作用。
GlobalsNoCondition GlobalVariableTemp(GlobalsNoCondition.mq5)=true / ok
GlobalsNoCondition 1 GlobalVariableTemp(GlobalsNoCondition.mq5)=false / GLOBALVARIABLE_EXISTS(4502)
GlobalsNoCondition GlobalVariableSet(GlobalsNoCondition.mq5,count+1)=2021.08.31 17:47:17 / ok
GlobalsNoCondition Copy 0 is working [0]...
GlobalsNoCondition 1 GlobalVariableSet(GlobalsNoCondition.mq5,count+1)=2021.08.31 17:47:17 / ok
GlobalsNoCondition 1 Copy 0 is working [0]...
GlobalsNoCondition Copy 0 is working [1]...
GlobalsNoCondition 1 Copy 0 is working [1]...
GlobalsNoCondition Copy 0 is working [2]...
GlobalsNoCondition 1 Copy 0 is working [2]...
GlobalsNoCondition Copy 0 is working [3]...
GlobalsNoCondition 1 Copy 0 is working [3]...
GlobalsNoCondition Copy 0 (out of 1) is stopping
GlobalsNoCondition GlobalVariableSet(GlobalsNoCondition.mq5,last-1)=2021.08.31 17:47:26 / ok
GlobalsNoCondition 1 Count underflow
两个副本都 “认为” 它们是编号0(工作循环输出 “Copy 0”),并且它们的总数错误地等于1,因为这是两个副本都存储在计数器变量中的值。
正是因为这个原因,当服务停止(“全部停止” 命令)时,我们收到了关于不正确状态的消息(“Count underflow”):毕竟,每个副本都试图将计数器减1,结果,第二个执行的副本得到了一个负值。
为了解决这个问题,需要使用 GlobalVariableSetOnCondition
函数。基于之前服务的源代码,准备了一个改进版本 GlobalsWithCondition.mq5
。总的来说,它重现了其前身的逻辑,但有一些显著的区别。
与仅仅调用 GlobalVariableSet
来增加计数器不同,必须编写一个更复杂的结构。
c
const int maxRetries = 5;
int retry = 0;
while(count < limit && retry < maxRetries)
{
Delay();
if(PRTF(GlobalVariableSetOnCondition(__FILE__, count + 1, count))) break;
// 条件不满足(计数已过时),赋值失败,
// 如果循环未超过限制,用新条件再试一次
count = (int)GlobalVariableGet(__FILE__);
PrintFormat("Counter is already altered by other instance: %d", count);
retry++;
}
if(count == limit || retry == maxRetries)
{
PrintFormat("Start failed: count: %d, retries: %d", count, retry);
return;
}
...
由于 GlobalVariableSetOnCondition
函数可能不会写入新的计数器值(如果旧值已经过时),我们在循环中再次读取全局变量,并重复尝试递增它,直到超过最大允许的计数器值。循环条件还限制了尝试的次数。如果循环以违反其中一个条件结束,那么计数器更新失败,程序不应继续运行。
同步策略
理论上,有几种标准策略来实现共享资源的获取。
第一种是软检查资源是否空闲,然后仅在资源在那一刻空闲时锁定它。如果资源繁忙,算法会在一定时间后计划下一次尝试,并且此时它会从事其他任务(这就是为什么这种方法对于有多个活动/责任领域的程序更可取)。在转换为 GlobalVariableSetOnCondition
函数的过程中,这种行为模式的类似方式是单次调用,没有循环,失败时退出当前块。变量的更改被推迟到 “更好的时候”。
第二种策略更具持久性,并且在我们的脚本中应用了这种策略。这是一个循环,它会在给定的次数或预定义的时间(资源的允许超时时间)内重复请求资源。如果循环结束且未达到积极结果(调用 GlobalVariableSetOnCondition
函数从未返回 true
),程序也会退出当前块,并且可能计划稍后再试一次。
最后,第三种策略是最严格的,涉及 “坚持到底” 地请求资源。可以将其视为一个带有函数调用的无限循环。这种方法在专注于一个特定任务并且没有获取到资源就无法继续工作的程序中是有意义的。在MQL5中,为此使用 while(!IsStopped())
循环,并且不要忘记在内部调用 Sleep
。
这里需要注意 “硬” 获取多个资源时的潜在问题。想象一下,一个MQL程序修改多个全局变量(理论上,这是一种常见情况)。如果它的一个副本获取了一个变量,而第二个副本获取了另一个变量,并且两者都将等待释放,那么它们将相互阻塞(死锁)。
基于上述内容,共享全局变量和其他资源(例如文件)应该仔细设计,并分析是否存在锁定以及所谓的 “竞态条件”,当程序的并行执行导致不确定的结果(取决于它们的工作顺序)时就会出现这种情况。
在新版本服务的工作循环完成后,计数器递减算法也以类似的方式进行了更改。
c
retry = 0;
int last = (int)GlobalVariableGet(__FILE__);
while(last > 0 && retry < maxRetries)
{
PrintFormat("Copy %d (out of %d) is stopping", count, last);
Delay();
if(PRTF(GlobalVariableSetOnCondition(__FILE__, last - 1, last))) break;
last = (int)GlobalVariableGet(__FILE__);
retry++;
}
if(last <= 0)
{
PrintFormat("Unexpected exit: %d", last);
}
else
{
PrintFormat("Stopped copy %d: count: %d, retries: %d", count, last, retry);
}
作为一个实验,让我们为新服务创建三个实例。在每个实例的设置中,在“限制”参数里,我们指定2个实例(以便在变化的条件下进行测试)。回想一下,创建每个实例会立即启动它,而这是我们不需要的,所以每个新创建的实例都应该被停止。
这些实例将获得默认名称“GlobalsWithCondition”、“GlobalsWithCondition 1”和“GlobalsWithCondition 2”。
当一切准备就绪后,我们同时运行所有实例,在日志中会得到类似这样的内容。
GlobalsWithCondition 2 GlobalVariableTemp(GlobalsWithCondition.mq5)= »
» false / GLOBALVARIABLE_EXISTS(4502)
GlobalsWithCondition 1 GlobalVariableTemp(GlobalsWithCondition.mq5)= »
» false / GLOBALVARIABLE_EXISTS(4502)
GlobalsWithCondition GlobalVariableTemp(GlobalsWithCondition.mq5)=true / ok
GlobalsWithCondition GlobalVariableSetOnCondition(GlobalsWithCondition.mq5,count+1,count)= »
» true / ok
GlobalsWithCondition 1 GlobalVariableSetOnCondition(GlobalsWithCondition.mq5,count+1,count)= »
» false / GLOBALVARIABLE_NOT_FOUND(4501)
GlobalsWithCondition 2 GlobalVariableSetOnCondition(GlobalsWithCondition.mq5,count+1,count)= »
» false / GLOBALVARIABLE_NOT_FOUND(4501)
GlobalsWithCondition 1 Counter is already altered by other instance: 1
GlobalsWithCondition Copy 0 is working [0]...
GlobalsWithCondition 2 Counter is already altered by other instance: 1
GlobalsWithCondition 1 GlobalVariableSetOnCondition(GlobalsWithCondition.mq5,count+1,count)=true / ok
GlobalsWithCondition 1 Copy 1 is working [0]...
GlobalsWithCondition 2 GlobalVariableSetOnCondition(GlobalsWithCondition.mq5,count+1,count)= »
» false / GLOBALVARIABLE_NOT_FOUND(4501)
GlobalsWithCondition 2 Counter is already altered by other instance: 2
GlobalsWithCondition 2 Start failed: count: 2, retries: 2
GlobalsWithCondition Copy 0 is working [1]...
GlobalsWithCondition 1 Copy 1 is working [1]...
GlobalsWithCondition Copy 0 is working [2]...
GlobalsWithCondition 1 Copy 1 is working [2]...
GlobalsWithCondition Copy 0 is working [3]...
GlobalsWithCondition 1 Copy 1 is working [3]...
GlobalsWithCondition Copy 0 (out of 2) is stopping
GlobalsWithCondition GlobalVariableSetOnCondition(GlobalsWithCondition.mq5,last-1,last)=true / ok
GlobalsWithCondition Stopped copy 0: count: 2, retries: 0
GlobalsWithCondition 1 Copy 1 (out of 1) is stopping
GlobalsWithCondition 1 GlobalVariableSetOnCondition(GlobalsWithCondition.mq5,last-1,last)=true / ok
GlobalsWithCondition 1 Stopped copy 1: count: 1, retries: 0
首先,请注意并行运行程序的上下文切换所产生的随机但直观的效果演示。创建临时变量的第一个实例是没有编号的“GlobalsWithCondition”:这可以从GlobalVariableTemp
函数的结果为true
看出来。然而,在日志中,这一行仅占据第三个位置,前两行包含了在名为“GlobalsWithCondition 1”和“GlobalsWithCondition 2”的副本中调用相同函数的结果;在这些副本中,GlobalVariableTemp
函数返回false
。这意味着这些副本后来才检查变量,尽管它们的线程随后超过了没有编号的“GlobalsWithCondition”线程,并在日志中先出现。
但让我们回到我们的主要程序计数算法。“GlobalsWithCondition”实例是第一个通过检查的,并以内部标识符“Copy 0”开始工作(我们无法从服务代码中得知用户给实例取了什么名字:至少在目前的MQL5 API中没有这样的函数)。
多亏了GlobalVariableSetOnCondition
函数,在实例1和2(“GlobalsWithCondition 1”、“GlobalsWithCondition 2”)中,检测到了计数器被修改的事实:开始时计数器为0,但“GlobalsWithCondition”将其增加了1。两个较晚的实例都输出了消息“Counter is already altered by other instance: 1”。其中一个实例(“GlobalsWithCondition 1”)领先于实例2,成功从变量中获取了新值1并将其增加到2。这由GlobalVariableSetOnCondition
的成功调用(返回true
)表明。并且,出现了关于它开始工作的消息“Copy 1 is working”。
内部计数器的值与外部实例编号相同纯属巧合。很有可能“GlobalsWithCondition 2”在“GlobalsWithCondition 1”之前启动(或者在有三个副本的情况下以其他某种顺序启动)。那么外部和内部编号就会不同。你可以多次重复启动和停止所有服务的实验,并且实例递增计数器变量的顺序很可能会不同。但无论如何,总数限制会阻止一个多余的实例运行。
当“GlobalsWithCondition 2”的最后一个实例被授予对全局变量的访问权限时,其中已经存储了值2。由于这是我们设置的限制,程序不会启动。
GlobalVariableSetOnCondition(GlobalsWithCondition.mq5,count+1,count)= »
» false / GLOBALVARIABLE_NOT_FOUND(4501)
Counter is already altered by other instance: 2
Start failed: count: 2, retries: 2
接下来,“GlobalsWithCondition”和“GlobalsWithCondition 1”的副本在工作循环中“运行”,直到服务停止。
你可以尝试仅停止一个实例。然后就可以启动另一个之前由于超过配额而被禁止执行的实例。
当然,所提出的防止并行修改的版本仅对协调你自己程序的行为有效,但对于限制演示版本的单个副本是不够的,因为用户可以简单地删除全局变量。为此,全局变量可以以不同的方式使用——与图表ID相关:一个MQL程序只要其创建的全局变量包含其图表ID就会一直运行。控制共享数据(计数器和其他信息)的其他方法由资源和数据库提供。
全局变量刷新到磁盘
为了优化性能,在终端运行期间,全局变量会存于内存之中。不过,我们知道,除临时变量外的所有全局变量在会话之间会存储于一个特殊文件里。通常,在终端关闭时,变量会被写入文件。但要是计算机突然崩溃,数据就可能丢失。所以,为确保在任何意外情况下数据的安全,强制启动写入操作会很有用。为此,MQL5 API 提供了 GlobalVariablesFlush
函数。
c
void GlobalVariablesFlush()
该函数会强制将全局变量的内容写入磁盘。此函数没有参数,也不返回任何值。
在脚本 GlobalsFlush.mq5
中给出了一个最简单的示例。
c
void OnStart()
{
GlobalVariablesFlush();
}
借助这个脚本,若有需要,你能随时将变量刷新到磁盘。你可以使用自己喜欢的文件管理器,会发现运行脚本后,gvariables.dat
文件的日期和时间会立刻改变。不过要注意,只有自上次保存后,全局变量以任何方式被编辑过或者只是被读取过(这会改变访问时间),文件才会更新。
对于那些长时间开启终端,并且其中运行着会修改全局变量的程序的用户来说,这个脚本很有用。