Sự kiện bộ đếm thời gian: OnTimer
Sự kiện OnTimer
là một trong những sự kiện tiêu chuẩn được hỗ trợ bởi các chương trình MQL5 (xem phần Tổng quan về các hàm xử lý sự kiện). Để nhận sự kiện bộ đếm thời gian trong mã chương trình, bạn nên mô tả một hàm với nguyên mẫu sau.
void OnTimer(void)
Sự kiện OnTimer
được tạo định kỳ bởi terminal khách hàng cho một Expert Advisor hoặc chỉ báo đã kích hoạt bộ đếm thời gian bằng các hàm EventSetTimer hoặc EventSetMillisecondTimer (xem phần tiếp theo).
Chú ý! Trong các chỉ báo phụ thuộc được tạo bằng cách gọi
iCustom
hoặcIndicatorCreate
từ các chương trình khác, bộ đếm thời gian không hoạt động và sự kiệnOnTimer
không được tạo. Đây là một hạn chế kiến trúc của MetaTrader 5.
Cần hiểu rằng sự hiện diện của một bộ đếm thời gian được bật và trình xử lý OnTimer
không làm cho chương trình MQL trở thành đa luồng. Không có quá một luồng được cấp phát cho mỗi chương trình MQL (một chỉ báo thậm chí có thể chia sẻ luồng với các chỉ báo khác trên cùng biểu tượng), vì vậy việc gọi OnTimer
và các trình xử lý khác luôn diễn ra tuần tự, phù hợp với hàng đợi sự kiện. Nếu một trong các trình xử lý, bao gồm OnTimer
, bắt đầu các tính toán kéo dài, điều này sẽ tạm dừng việc thực thi tất cả các sự kiện khác và các phần mã chương trình.
Nếu bạn cần tổ chức xử lý dữ liệu song song, bạn nên chạy nhiều chương trình MQL đồng thời (có thể là các phiên bản của cùng một chương trình trên các biểu đồ hoặc đối tượng biểu đồ khác nhau) và trao đổi lệnh và dữ liệu giữa chúng bằng giao thức riêng, ví dụ, sử dụng sự kiện tùy chỉnh.
Ví dụ, hãy tạo các lớp có thể tổ chức nhiều bộ đếm thời gian logic trong một chương trình. Chu kỳ của tất cả các bộ đếm thời gian logic sẽ được đặt dưới dạng bội số của chu kỳ cơ bản, tức là chu kỳ của một bộ đếm thời gian phần cứng duy nhất cung cấp sự kiện cho trình xử lý tiêu chuẩn OnTimer
. Trong trình xử lý này, chúng ta phải gọi một phương thức nhất định của lớp mới MultiTimer
sẽ quản lý tất cả các bộ đếm thời gian logic.
void OnTimer()
{
// gọi phương thức MultiTimer để kiểm tra và gọi các bộ đếm thời gian phụ thuộc khi cần
MultiTimer::onTimer();
}
2
3
4
5
Lớp MultiTimer
và các lớp liên quan của các bộ đếm thời gian riêng lẻ sẽ được kết hợp trong một tệp, MultiTimer.mqh
.
Lớp cơ sở cho các bộ đếm thời gian hoạt động sẽ là TimerNotification
. Nói một cách nghiêm ngặt, đây có thể là một giao diện, nhưng việc đưa một số chi tiết của triển khai chung vào đó rất tiện lợi: cụ thể, lưu trữ giá trị của bộ đếm chronometer
, sử dụng nó chúng ta sẽ đảm bảo bộ đếm thời gian kích hoạt với một bội số nhất định của chu kỳ tương đối của bộ đếm thời gian chính, cũng như một phương thức để kiểm tra thời điểm bộ đếm thời gian nên kích hoạt isTimeCome
. Đó là lý do tại sao TimerNotification
là một lớp trừu tượng. Nó thiếu triển khai của hai phương thức ảo: notify
- cho các hành động khi bộ đếm thời gian kích hoạt - và getInterval
để lấy bội số xác định chu kỳ của một bộ đếm thời gian cụ thể so với chu kỳ của bộ đếm thời gian chính.
class TimerNotification
{
protected:
int chronometer; // bộ đếm kiểm tra bộ đếm thời gian (lần gọi isTimeCome)
public:
TimerNotification(): chronometer(0)
{
}
// sự kiện hoạt động của bộ đếm thời gian
// phương thức ảo thuần túy, cần được mô tả trong các lớp kế thừa
virtual void notify() = 0;
// trả về chu kỳ của bộ đếm thời gian (có thể thay đổi trong quá trình chạy)
// phương thức ảo thuần túy, cần được mô tả trong các lớp kế thừa
virtual int getInterval() = 0;
// kiểm tra xem đã đến lúc bộ đếm thời gian kích hoạt chưa, và nếu có, gọi notify
virtual bool isTimeCome()
{
if(chronometer >= getInterval() - 1)
{
chronometer = 0; // đặt lại bộ đếm
notify(); // thông báo mã ứng dụng
return true;
}
++chronometer;
return false;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Toàn bộ logic được cung cấp trong phương thức isTimeCome
. Mỗi lần nó được gọi, bộ đếm chronometer
tăng lên, và nếu nó đạt đến lần lặp cuối cùng theo phương thức getInterval
, phương thức notify
được gọi để thông báo mã ứng dụng.
Ví dụ, nếu bộ đếm thời gian chính được khởi động với chu kỳ 1 giây (EventSetTimer(1)
), thì đối tượng con TimerNotification,
trả về 5 từ getInterval
, sẽ nhận các cuộc gọi đến phương thức notify
của nó mỗi 5 giây.
Như đã nói, các đối tượng bộ đếm thời gian này sẽ được quản lý bởi đối tượng quản lý MultiTimer
. Chúng ta chỉ cần một đối tượng như vậy. Do đó, hàm tạo của nó được khai báo là protected, và một phiên bản duy nhất được tạo tĩnh trong lớp.
class MultiTimer
{
protected:
static MultiTimer _mainTimer;
MultiTimer()
{
}
...
2
3
4
5
6
7
8
9
Bên trong lớp này, chúng ta tổ chức lưu trữ mảng các đối tượng TimerNotification
(chúng ta sẽ thấy cách nó được điền trong vài đoạn sau). Khi đã có mảng, chúng ta có thể dễ dàng viết phương thức checkTimers
lặp qua tất cả các bộ đếm thời gian logic. Để truy cập bên ngoài, phương thức này được sao chép bởi phương thức tĩnh công khai onTimer
, mà chúng ta đã thấy trong trình xử lý toàn cục OnTimer
. Vì phiên bản quản lý duy nhất được tạo tĩnh, chúng ta có thể truy cập nó từ một phương thức tĩnh.
...
TimerNotification *subscribers[];
void checkTimers()
{
int n = ArraySize(subscribers);
for(int i = 0; i < n; ++i)
{
if(CheckPointer(subscribers[i]) != POINTER_INVALID)
{
subscribers[i].isTimeCome();
}
}
}
public:
static void onTimer()
{
_mainTimer.checkTimers();
}
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Đối tượng TimerNotification
được thêm vào mảng subscribers
bằng phương thức bind
.
void bind(TimerNotification &tn)
{
int i, n = ArraySize(subscribers);
for(i = 0; i < n; ++i)
{
if(subscribers[i] == &tn) return; // đã có đối tượng như vậy
if(subscribers[i] == NULL) break; // tìm thấy một ô trống
}
if(i == n)
{
ArrayResize(subscribers, n + 1);
}
else
{
n = i;
}
subscribers[n] = &tn;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Phương thức này được bảo vệ khỏi việc thêm trùng lặp đối tượng, và nếu có thể, con trỏ được đặt vào một phần tử trống của mảng, nếu có, điều này loại bỏ nhu cầu mở rộng mảng. Các phần tử trống trong mảng có thể xuất hiện nếu bất kỳ đối tượng TimerNotification
nào bị xóa bằng phương thức unbind
(các bộ đếm thời gian có thể được sử dụng không thường xuyên).
void unbind(TimerNotification &tn)
{
const int n = ArraySize(subscribers);
for(int i = 0; i < n; ++i)
{
if(subscribers[i] == &tn)
{
subscribers[i] = NULL;
return;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
Lưu ý rằng trình quản lý không sở hữu đối tượng bộ đếm thời gian và không cố gắng gọi delete
. Nếu bạn định đăng ký các đối tượng bộ đếm thời gian được cấp phát động trong trình quản lý, bạn có thể thêm mã sau bên trong if
trước khi đặt về 0:
if(CheckPointer(subscribers[i]) == POINTER_DYNAMIC) delete subscribers[i];
Bây giờ còn lại là tìm hiểu cách chúng ta có thể tổ chức các cuộc gọi bind
/unbind
một cách tiện lợi, để không làm nặng mã ứng dụng với các thao tác tiện ích này. Nếu bạn thực hiện thủ công, rất dễ quên tạo hoặc ngược lại, xóa bộ đếm thời gian ở đâu đó.
Hãy phát triển lớp SingleTimer
kế thừa từ TimerNotification
, trong đó chúng ta triển khai các cuộc gọi bind
và unbind
từ hàm tạo và hàm hủy, tương ứng. Ngoài ra, chúng ta mô tả trong đó biến multiplier
để lưu trữ chu kỳ bộ đếm thời gian.
class SingleTimer: public TimerNotification
{
protected:
int multiplier;
MultiTimer *owner;
public:
// tạo bộ đếm thời gian với bội số chu kỳ cơ bản được chỉ định, tùy chọn tạm dừng
// tự động đăng ký đối tượng trong trình quản lý
SingleTimer(const int m, const bool paused = false): multiplier(m)
{
owner = &MultiTimer::_mainTimer;
if(!paused) owner.bind(this);
}
// tự động ngắt kết nối đối tượng khỏi trình quản lý
~SingleTimer()
{
owner.unbind(this);
}
// trả về chu kỳ bộ đếm thời gian
virtual int getInterval() override
{
return multiplier;
}
// tạm dừng bộ đếm thời gian này
virtual void stop()
{
owner.unbind(this);
}
// tiếp tục bộ đếm thời gian này
virtual void start()
{
owner.bind(this);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
Tham số thứ hai của hàm tạo (paused
) cho phép bạn tạo một đối tượng, nhưng không khởi động bộ đếm thời gian ngay lập tức. Một bộ đếm thời gian bị trì hoãn như vậy sau đó có thể được kích hoạt bằng phương thức start
.
Mô hình đăng ký một số đối tượng vào sự kiện trong các đối tượng khác là một trong những mẫu thiết kế phổ biến trong OOP và được gọi là "publisher/subscriber".
Điều quan trọng cần lưu ý là lớp này cũng là trừu tượng vì nó không triển khai phương thức notify
. Dựa trên SingleTimer
, hãy mô tả các lớp của bộ đếm thời gian với chức năng bổ sung.
Hãy bắt đầu với lớp CountableTimer
. Nó cho phép bạn chỉ định số lần nó nên kích hoạt, sau đó nó sẽ tự động dừng lại. Với nó, đặc biệt, dễ dàng tổ chức một hành động đơn lẻ bị trì hoãn. Hàm tạo của CountableTimer
có các tham số để thiết lập chu kỳ bộ đếm thời gian, cờ tạm dừng và số lần thử lại. Theo mặc định, số lần lặp lại không bị giới hạn, vì vậy lớp này sẽ trở thành nền tảng cho hầu hết các bộ đếm thời gian ứng dụng.
class CountableTimer: public MultiTimer::SingleTimer
{
protected:
const uint repeat;
uint count;
public:
CountableTimer(const int m, const uint r = UINT_MAX, const bool paused = false):
SingleTimer(m, paused), repeat(r), count(0) { }
virtual bool isTimeCome() override
{
if(count >= repeat && repeat != UINT_MAX)
{
stop();
return false;
}
// ủy quyền kiểm tra thời gian cho lớp cha,
// chỉ tăng bộ đếm của chúng ta nếu bộ đếm thời gian kích hoạt (trả về true)
return SingleTimer::isTimeCome() && (bool)++count;
}
// đặt lại bộ đếm của chúng ta khi dừng
virtual void stop() override
{
SingleTimer::stop();
count = 0;
}
uint getCount() const
{
return count;
}
uint getRepeat() const
{
return repeat;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Để sử dụng CountableTimer
, chúng ta phải mô tả lớp dẫn xuất trong chương trình của mình như sau.
// MultipleTimers.mq5
class MyCountableTimer: public CountableTimer
{
public:
MyCountableTimer(const int s, const uint r = UINT_MAX):
CountableTimer(s, r) { }
virtual void notify() override
{
Print(__FUNCSIG__, multiplier, " ", count);
}
};
2
3
4
5
6
7
8
9
10
11
12
Trong triển khai này của phương thức notify
, chúng ta chỉ ghi lại chu kỳ bộ đếm thời gian và số lần nó kích hoạt. Nhân tiện, đây là một đoạn của chỉ báo MultipleTimers.mq5
, mà chúng ta sẽ sử dụng làm ví dụ hoạt động.
Hãy gọi lớp thứ hai kế thừa từ SingleTimer
là FunctionalTimer
. Mục đích của nó là cung cấp một triển khai bộ đếm thời gian đơn giản cho những người thích phong cách lập trình chức năng và không muốn viết các lớp dẫn xuất. Hàm tạo của lớp FunctionalTimer
sẽ nhận, ngoài chu kỳ, một con trỏ đến một hàm thuộc loại đặc biệt, TimerHandler
.
// MultiTimer.mqh
typedef bool (*TimerHandler)(void);
class FunctionalTimer: public MultiTimer::SingleTimer
{
TimerHandler func;
public:
FunctionalTimer(const int m, TimerHandler f):
SingleTimer(m), func(f) { }
virtual void notify() override
{
if(func != NULL)
{
if(!func())
{
stop();
}
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Trong triển khai này của phương thức notify
, đối tượng gọi hàm bằng con trỏ. Với lớp như vậy, chúng ta có thể định nghĩa một macro mà khi đặt trước một khối câu lệnh trong dấu ngoặc nhọn, sẽ "biến" nó thành phần thân của hàm bộ đếm thời gian.
// MultiTimer.mqh
#define OnTimerCustom(P) OnTimer##P(); \
FunctionalTimer ft##P(P, OnTimer##P); \
bool OnTimer##P()
2
3
4
Sau đó trong mã ứng dụng bạn có thể viết như thế này:
// MultipleTimers.mq5
bool OnTimerCustom(3)
{
Print(__FUNCSIG__);
return true; // tiếp tục bộ đếm thời gian
}
2
3
4
5
6
Cấu trúc này khai báo một bộ đếm thời gian với chu kỳ 3 và một tập hợp các lệnh bên trong dấu ngoặc (ở đây, chỉ in vào nhật ký). Nếu hàm này trả về false
, bộ đếm thời gian này sẽ dừng lại.
Hãy xem xét chỉ báo MultipleTimers.mq5
kỹ hơn. Vì nó không cung cấp hình ảnh hóa, chúng ta sẽ chỉ định số lượng biểu đồ bằng 0.
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots 0
2
3
Để sử dụng các lớp của bộ đếm thời gian logic, chúng ta bao gồm tệp tiêu đề MultiTimer.mqh
và thêm một biến đầu vào cho chu kỳ bộ đếm thời gian cơ bản (toàn cục).
#include <MQL5Book/MultiTimer.mqh>
input int BaseTimerPeriod = 1;
2
3
Bộ đếm thời gian cơ bản được khởi động trong OnInit
.
void OnInit()
{
Print(__FUNCSIG__, " ", BaseTimerPeriod, " Seconds");
EventSetTimer(BaseTimerPeriod);
}
2
3
4
5
Nhắc lại rằng hoạt động của tất cả các bộ đếm thời gian logic được đảm bảo bởi việc chặn sự kiện toàn cục OnTimer
.
void OnTimer()
{
MultiTimer::onTimer();
}
2
3
4
Ngoài lớp ứng dụng bộ đếm thời gian MyCountableTimer
ở trên, hãy mô tả một lớp khác của bộ đếm thời gian bị tạm dừng MySuspendedTimer
.
class MySuspendedTimer: public CountableTimer
{
public:
MySuspendedTimer(const int s, const uint r = UINT_MAX):
CountableTimer(s, r, true) { }
virtual void notify() override
{
Print(__FUNCSIG__, multiplier, " ", count);
if(count == repeat - 1) // thực thi lần cuối
{
Print("Forcing all timers to stop");
EventKillTimer();
}
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Một chút nữa chúng ta sẽ thấy cách nó khởi động. Cũng cần lưu ý ở đây rằng sau khi đạt đến số lần hoạt động được chỉ định, bộ đếm thời gian này sẽ tắt tất cả các bộ đếm thời gian bằng cách gọi EventKillTimer
.
Bây giờ hãy chỉ ra cách (trong bối cảnh toàn cục) các đối tượng của các bộ đếm thời gian khác nhau của hai lớp này được mô tả.
MySuspendedTimer st(1, 5);
MyCountableTimer t1(2);
MyCountableTimer t2(4);
2
3
Bộ đếm thời gian st
của lớp MySuspendedTimer
có chu kỳ 1 (1*BaseTimerPeriod
) và nên dừng sau 5 lần hoạt động.
Các bộ đếm thời gian t1
và t2
của lớp MyCountableTimer
có chu kỳ 2 (2 * BaseTimerPeriod
) và 4 (4 * BaseTimerPeriod
), tương ứng. Với giá trị mặc định BaseTimerPeriod = 1
, tất cả các chu kỳ đại diện cho giây. Hai bộ đếm thời gian này được khởi động ngay sau khi chương trình bắt đầu.
Chúng ta cũng sẽ tạo hai bộ đếm thời gian theo phong cách chức năng.
bool OnTimerCustom(5)
{
Print(__FUNCSIG__);
st.start(); // khởi động bộ đếm thời gian bị trì hoãn
return false; // và dừng đối tượng bộ đếm thời gian này
}
bool OnTimerCustom(3)
{
Print(__FUNCSIG__);
return true; // bộ đếm thời gian này tiếp tục chạy
}
2
3
4
5
6
7
8
9
10
11
12
Lưu ý rằng OnTimerCustom5
chỉ có một nhiệm vụ: 5 chu kỳ sau khi chương trình bắt đầu, nó cần khởi động bộ đếm thời gian bị trì hoãn st
và chấm dứt thực thi của chính nó. Xét rằng bộ đếm thời gian bị trì hoãn nên vô hiệu hóa tất cả các bộ đếm thời gian sau 5 chu kỳ, chúng ta có 10 giây hoạt động chương trình với cài đặt mặc định.
Bộ đếm thời gian OnTimerCustom3
nên kích hoạt ba lần trong khoảng thời gian này.
Vậy, chúng ta có 5 bộ đếm thời gian với các chu kỳ khác nhau: 1, 2, 3, 4, 5 giây.
Hãy phân tích một ví dụ về những gì được xuất ra nhật ký (dấu thời gian được hiển thị sơ đồ ở bên phải).
// thời gian
17:08:45.174 void OnInit() 1 Seconds |
17:08:47.202 void MyCountableTimer::notify()2 0 |
17:08:48.216 bool OnTimer3() |
17:08:49.230 void MyCountableTimer::notify()2 1 |
17:08:49.230 void MyCountableTimer::notify()4 0 |
17:08:50.244 bool OnTimer5() |
17:08:51.258 void MyCountableTimer::notify()2 2 |
17:08:51.258 bool OnTimer3() |
17:08:51.258 void MySuspendedTimer::notify()1 0 |
17:08:52.272 void MySuspendedTimer::notify()1 1 |
17:08:53.286 void MyCountableTimer::notify()2 3 |
17:08:53.286 void MyCountableTimer::notify()4 1 |
17:08:53.286 void MySuspendedTimer::notify()1 2 |
17:08:54.300 bool OnTimer3() |
17:08:54.300 void MySuspendedTimer::notify()1 3 |
17:08:55.314 void MyCountableTimer::notify()2 4 |
17:08:55.314 void MySuspendedTimer::notify()1 4 |
17:08:55.314 Forcing all timers to stop |
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Thông điệp đầu tiên từ bộ đếm thời gian hai giây đến, như dự kiến, khoảng 2 giây sau khi bắt đầu (chúng ta nói "khoảng" vì bộ đếm thời gian phần cứng có giới hạn về độ chính xác và ngoài ra, tải máy tính khác ảnh hưởng đến việc thực thi). Một giây sau, bộ đếm thời gian ba giây kích hoạt lần đầu tiên. Lần kích hoạt thứ hai của bộ đếm thời gian hai giây trùng với đầu ra đầu tiên từ bộ đếm thời gian bốn giây. Sau khi thực thi một lần của bộ đếm thời gian năm giây, các thông điệp từ bộ đếm thời gian một giây bắt đầu xuất hiện đều đặn trong nhật ký (bộ đếm của nó tăng từ 0 đến 4). Ở lần lặp cuối cùng, nó dừng tất cả các bộ đếm thời gian.