Ép buộc ghi bộ đệm vào đĩa
Việc ghi và đọc tệp trong MQL5 được lưu vào bộ đệm. Điều này có nghĩa là một bộ đệm nhất định trong bộ nhớ được duy trì cho dữ liệu, nhờ đó tăng hiệu quả công việc. Vì vậy, dữ liệu được truyền qua các lệnh gọi hàm trong quá trình ghi sẽ vào bộ đệm đầu ra, và chỉ sau khi bộ đệm đầy, việc ghi vật lý vào đĩa mới diễn ra. Khi đọc, ngược lại, dữ liệu được đọc từ đĩa vào bộ đệm nhiều hơn lượng mà chương trình yêu cầu qua các hàm (nếu không phải là cuối tệp), và các thao tác đọc tiếp theo (rất có khả năng xảy ra) sẽ nhanh hơn.
Bộ đệm là một công nghệ tiêu chuẩn được sử dụng trong hầu hết các ứng dụng và ở cấp độ hệ điều hành. Tuy nhiên, bên cạnh những ưu điểm, bộ đệm cũng có nhược điểm.
Cụ thể, nếu tệp được sử dụng như một phương tiện trao đổi dữ liệu giữa các chương trình, việc ghi bị trì hoãn có thể làm chậm đáng kể quá trình giao tiếp và khiến nó ít dự đoán được hơn, vì kích thước bộ đệm có thể khá lớn, và tần suất "đổ" nó vào đĩa có thể được điều chỉnh theo một số thuật toán.
Ví dụ, trong MetaTrader 5, có một danh mục chương trình MQL để sao chép tín hiệu giao dịch từ một phiên bản của terminal sang phiên bản khác. Chúng thường sử dụng tệp để truyền thông tin, và điều rất quan trọng đối với chúng là bộ đệm không làm chậm quá trình. Trong trường hợp này, MQL5 cung cấp hàm FileFlush
.
void FileFlush(int handle)
Hàm này thực hiện việc ép buộc ghi tất cả dữ liệu còn lại trong bộ đệm I/O của tệp vào đĩa cho tệp có mô tả handle
.
Nếu bạn không sử dụng hàm này, một phần dữ liệu "gửi" từ chương trình có thể, trong trường hợp xấu nhất, chỉ được ghi vào đĩa khi tệp được đóng.
Tính năng này cung cấp sự đảm bảo cao hơn cho việc bảo vệ dữ liệu quý giá trong trường hợp xảy ra sự cố bất ngờ (như hệ điều hành hoặc chương trình bị treo). Tuy nhiên, mặt khác, việc gọi FileFlush
thường xuyên trong quá trình ghi hàng loạt không được khuyến nghị, vì nó có thể ảnh hưởng tiêu cực đến hiệu suất.
Nếu tệp được mở ở chế độ hỗn hợp, đồng thời để ghi và đọc, hàm FileFlush
phải được gọi giữa các lần đọc và ghi vào tệp.
Ví dụ, hãy xem xét tập lệnh FileFlush.mq5
, trong đó chúng ta triển khai hai chế độ mô phỏng hoạt động của bộ sao chép giao dịch. Chúng ta sẽ cần chạy hai phiên bản của tập lệnh trên các biểu đồ khác nhau, với một phiên bản trở thành người gửi dữ liệu và phiên bản kia trở thành người nhận.
Tập lệnh có hai tham số đầu vào: EnableFlashing
cho phép so sánh hành động của các chương trình khi sử dụng hàm FileFlush
và không sử dụng nó, và UseCommonFolder
chỉ định việc cần tạo tệp đóng vai trò là phương tiện truyền dữ liệu, tùy chọn: trong thư mục của phiên bản terminal hiện tại hoặc trong thư mục chung (trong trường hợp sau, bạn có thể kiểm tra việc truyền dữ liệu giữa các terminal khác nhau).
#property script_show_inputs
input bool EnableFlashing = false;
input bool UseCommonFolder = false;
2
3
Hãy nhớ rằng để hiển thị hộp thoại với các biến đầu vào khi tập lệnh được khởi chạy, bạn phải thêm thuộc tính script_show_inputs
.
Tên của tệp trung chuyển được chỉ định trong biến dataport
. Tùy chọn UseCommonFolder
điều khiển cờ FILE_COMMON
được thêm vào tập hợp các chế độ chuyển đổi cho các tệp được mở trong hàm FileOpen
.
const string dataport = "MQL5Book/dataport";
const int flag = UseCommonFolder ? FILE_COMMON : 0;
2
Hàm chính OnStart
thực sự bao gồm hai phần: cài đặt cho tệp đã mở và một vòng lặp định kỳ gửi hoặc nhận dữ liệu.
Chúng ta sẽ cần chạy hai phiên bản của tập lệnh, và mỗi phiên bản sẽ có mô tả tệp riêng trỏ đến cùng một tệp trên đĩa nhưng được mở ở các chế độ khác nhau.
void OnStart()
{
bool modeWriter = true; // mặc định tập lệnh sẽ ghi dữ liệu
int count = 0; // số lần ghi/đọc đã thực hiện
// tạo mới hoặc đặt lại tệp cũ ở chế độ đọc, như một "người gửi"
int handle = PRTF(FileOpen(dataport,
FILE_BIN | FILE_WRITE | FILE_SHARE_READ | flag));
// nếu không thể ghi, rất có thể một phiên bản khác của tập lệnh đã ghi vào tệp,
// vì vậy chúng ta thử mở nó để đọc
if(handle == INVALID_HANDLE)
{
// nếu có thể mở tệp để đọc, chúng ta sẽ tiếp tục làm việc như một "người nhận"
handle = PRTF(FileOpen(dataport,
FILE_BIN | FILE_READ | FILE_SHARE_WRITE | FILE_SHARE_READ | flag));
if(handle == INVALID_HANDLE)
{
Print("Can't open file"); // có gì đó không ổn
return;
}
modeWriter = false; // chuyển đổi mô hình/vai trò
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Ban đầu, chúng ta cố gắng mở tệp ở chế độ FILE_WRITE
, không chia sẻ quyền ghi (FILE_SHARE_WRITE
), vì vậy phiên bản đầu tiên của tập lệnh đang chạy sẽ chiếm tệp và ngăn phiên bản thứ hai hoạt động ở chế độ ghi. Phiên bản thứ hai sẽ nhận lỗi và INVALID_HANDLE
sau lần gọi đầu tiên đến FileOpen
và sẽ thử mở tệp ở chế độ đọc (FILE_READ
) với lần gọi thứ hai của FileOpen
sử dụng cờ ghi song song FILE_SHARE_WRITE
. Lý tưởng nhất, điều này sẽ hoạt động. Sau đó, biến modeWriter
sẽ được đặt thành false
để chỉ ra vai trò thực tế của tập lệnh.
Vòng lặp hoạt động chính có cấu trúc như sau:
while(!IsStopped())
{
if(modeWriter)
{
// ...ghi dữ liệu kiểm tra
}
else
{
// ...đọc dữ liệu kiểm tra
}
Sleep(5000);
}
2
3
4
5
6
7
8
9
10
11
12
Vòng lặp được thực thi cho đến khi người dùng xóa tập lệnh khỏi biểu đồ theo cách thủ công: điều này sẽ được báo hiệu bởi hàm IsStopped. Bên trong vòng lặp, hành động được kích hoạt mỗi 5 giây bằng cách gọi hàm Sleep, "đóng băng" chương trình trong số mili giây được chỉ định (5000 trong trường hợp này). Điều này được thực hiện để dễ phân tích các thay đổi đang diễn ra và tránh ghi nhật ký trạng thái quá thường xuyên. Trong một chương trình thực tế không có nhật ký chi tiết, bạn có thể gửi dữ liệu mỗi 100 mili giây hoặc thậm chí thường xuyên hơn.
Dữ liệu được truyền sẽ bao gồm thời gian hiện tại (một giá trị datetime
, 8 byte). Trong nhánh đầu tiên của lệnh if(modeWriter)
, nơi tệp được ghi, chúng ta gọi FileWriteLong
với số đếm cuối cùng (lấy từ hàm TimeLocal), tăng bộ đếm thao tác lên 1 (count++
) và xuất trạng thái hiện tại vào nhật ký.
long temp = TimeLocal(); // lấy thời gian địa phương hiện tại datetime
FileWriteLong(handle, temp); // thêm nó vào tệp (mỗi 5 giây)
count++;
if(EnableFlashing)
{
FileFlush(handle);
}
Print(StringFormat("Written[%d]: %I64d", count, temp));
2
3
4
5
6
7
8
Điều quan trọng cần lưu ý là việc gọi hàm FileFlush
sau mỗi lần ghi chỉ được thực hiện nếu tham số đầu vào EnableFlashing
được đặt thành true
.
Trong nhánh thứ hai của toán tử if
, nơi chúng ta đọc dữ liệu, chúng ta đầu tiên đặt lại cờ lỗi nội bộ bằng cách gọi ResetLastError
. Điều này là cần thiết vì chúng ta sẽ đọc dữ liệu từ tệp miễn là còn dữ liệu. Khi không còn dữ liệu để đọc, chương trình sẽ nhận mã lỗi cụ thể 5015 (ERR_FILE_READERROR
).
Do các bộ định thời tích hợp của MQL5, bao gồm hàm Sleep
, có độ chính xác hạn chế (khoảng 10 ms), chúng ta không thể loại trừ tình huống mà hai lần ghi liên tiếp xảy ra giữa hai lần thử đọc tệp. Ví dụ, một lần đọc xảy ra lúc 10:00:00'200, và lần thứ hai lúc 10:00:05'210 (theo ký hiệu "giờ:phút:giây'mili giây"
). Trong trường hợp này, hai lần ghi xảy ra song song: một lúc 10:00:00'205, và lần thứ hai lúc 10:00:05'205, và cả hai đều rơi vào khoảng thời gian trên. Tình huống như vậy ít xảy ra nhưng có thể. Ngay cả với các khoảng thời gian chính xác tuyệt đối, hệ thống thời gian chạy MQL5 có thể buộc phải chọn giữa hai tập lệnh đang chạy (tập lệnh nào được gọi trước) nếu tổng số chương trình lớn và không đủ lõi xử lý cho tất cả.
MQL5 cung cấp bộ định thời có độ chính xác cao (đến micro giây), nhưng điều này không quan trọng đối với nhiệm vụ hiện tại.
Vòng lặp lồng nhau cần thiết vì một lý do nữa. Ngay sau khi tập lệnh được khởi chạy như một "người nhận" dữ liệu, nó phải xử lý tất cả các bản ghi từ tệp đã tích lũy kể từ khi khởi chạy "người gửi" (khó có khả năng cả hai tập lệnh có thể được khởi chạy đồng thời). Có lẽ ai đó sẽ thích một thuật toán khác: bỏ qua tất cả các bản ghi "cũ" và chỉ theo dõi các bản mới. Điều này có thể thực hiện được, nhưng tùy chọn "không mất mát" được triển khai ở đây.
ResetLastError();
while(true) // lặp miễn là còn dữ liệu và không có vấn đề
{
bool reportedEndBeforeRead = FileIsEnding(handle);
ulong reportedTellBeforeRead = FileTell(handle);
temp = FileReadLong(handle);
// nếu không còn dữ liệu, chúng ta sẽ nhận lỗi 5015 (ERR_FILE_READERROR)
if(_LastError) break; // thoát vòng lặp khi có bất kỳ lỗi nào
// tại đây dữ liệu được nhận mà không có lỗi
count++;
Print(StringFormat("Read[%d]: %I64d\t"
"(size=%I64d, before=%I64d(%s), after=%I64d)",
count, temp,
FileSize(handle), reportedTellBeforeRead,
(string)reportedEndBeforeRead, FileTell(handle)));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vui lòng lưu ý điểm sau. Siêu dữ liệu về tệp được mở để đọc, chẳng hạn như kích thước của nó, được trả về bởi hàm FileSize
(xem Lấy thuộc tính tệp) không thay đổi sau khi tệp được mở. Nếu một chương trình khác sau đó thêm nội dung vào tệp mà chúng ta mở để đọc, chiều dài "có thể phát hiện" của nó sẽ không được cập nhật ngay cả khi chúng ta gọi FileFlash
cho mô tả đọc. Có thể đóng và mở lại tệp (trước mỗi lần đọc, nhưng điều này không hiệu quả): sau đó chiều dài mới sẽ xuất hiện cho mô tả mới. Nhưng chúng ta sẽ làm mà không cần điều đó, với sự trợ giúp của một mẹo khác.
Mẹo là tiếp tục đọc dữ liệu bằng các hàm đọc (trong trường hợp của chúng ta là FileReadLong
) miễn là chúng trả về dữ liệu mà không có lỗi. Điều quan trọng là không sử dụng các hàm khác hoạt động trên siêu dữ liệu. Đặc biệt, do điểm cuối tệp chỉ đọc vẫn không đổi, việc kiểm tra bằng hàm FileIsEnding
(xem Kiểm soát vị trí trong tệp) sẽ trả về true
tại vị trí cũ, mặc dù tệp có thể được bổ sung từ một tiến trình khác. Hơn nữa, nỗ lực di chuyển con trỏ tệp nội bộ đến cuối (FileSeek(handle, 0, SEEK_END)
; đối với hàm FileSeek
, xem cùng phần) sẽ không nhảy đến điểm cuối thực tế của dữ liệu, mà đến vị trí lỗi thời nơi điểm cuối nằm tại thời điểm mở.
Hàm FileTell
(xem cùng phần) cho chúng ta biết vị trí thực sự bên trong tệp. Khi thông tin được thêm vào tệp từ một phiên bản khác của tập lệnh và được đọc trong vòng lặp này, con trỏ sẽ di chuyển ngày càng xa về bên phải, vượt quá, dù kỳ lạ thế nào, FileSize
. Để minh họa trực quan cách con trỏ di chuyển vượt quá kích thước tệp, hãy lưu giá trị của nó trước và sau khi gọi FileReadLong
, sau đó xuất các giá trị cùng với kích thước vào nhật ký.
Khi đọc bằng FileReadLong
tạo ra bất kỳ lỗi nào, vòng lặp bên trong sẽ thoát. Thoát vòng lặp thông thường ngụ ý lỗi 5015 (ERR_FILE_READERROR
). Đặc biệt, nó xảy ra khi không có dữ liệu nào để đọc tại vị trí hiện tại trong tệp.
Dữ liệu đọc thành công cuối cùng được xuất ra nhật ký, và dễ dàng so sánh nó với những gì tập lệnh gửi đã xuất ra đó.
Hãy chạy tập lệnh mới hai lần. Để phân biệt giữa các bản sao của nó, chúng ta sẽ thực hiện trên các biểu đồ của các công cụ khác nhau.
Khi chạy cả hai tập lệnh, điều quan trọng là phải tuân thủ cùng giá trị của tham số
UseCommonFolder
. Chúng ta sẽ để nó trong các bài kiểm tra của mình bằngfalse
vì chúng ta sẽ thực hiện mọi thứ trong một terminal. Việc truyền dữ liệu giữa các terminal khác nhau vớiUseCommonFolder
được đặt thànhtrue
được đề xuất để kiểm tra độc lập.
Đầu tiên, hãy chạy phiên bản đầu tiên trên biểu đồ EURUSD,H1, giữ nguyên tất cả cài đặt mặc định, bao gồm EnableFlashing=false
. Sau đó, chúng ta sẽ chạy phiên bản thứ hai trên biểu đồ XAUUSD,H1 (cũng với cài đặt mặc định). Nhật ký sẽ như sau (thời gian của bạn sẽ khác):
(EURUSD,H1) *
(EURUSD,H1) FileOpen(dataport,FILE_BIN|FILE_WRITE|FILE_SHARE_READ|flag)=1 / ok
(EURUSD,H1) Written[1]: 1629652995
(XAUUSD,H1) *
(XAUUSD,H1) FileOpen(dataport,FILE_BIN|FILE_WRITE|FILE_SHARE_READ|flag)=-1 / CANNOT_OPEN_FILE(5004)
(XAUUSD,H1) FileOpen(dataport,FILE_BIN|FILE_READ|FILE_SHARE_WRITE|FILE_SHARE_READ|flag)=1 / ok
(EURUSD,H1) Written[2]: 1629653000
(EURUSD,H1) Written[3]: 1629653005
(EURUSD,H1) Written[4]: 1629653010
(EURUSD,H1) Written[5]: 1629653015
2
3
4
5
6
7
8
9
10
Người gửi đã mở tệp thành công để ghi và bắt đầu gửi dữ liệu mỗi 5 giây, theo các dòng có từ "Written" và các giá trị tăng dần. Dưới 5 giây sau khi người gửi được khởi động, người nhận cũng được khởi động. Nó đưa ra thông báo lỗi vì không thể mở tệp để ghi. Nhưng sau đó nó đã mở tệp thành công để đọc. Tuy nhiên, không có bản ghi nào cho thấy nó có thể tìm thấy dữ liệu được truyền trong tệp. Dữ liệu vẫn "treo" trong bộ đệm của người gửi.
Hãy dừng cả hai tập lệnh và chạy lại chúng theo cùng trình tự: đầu tiên, chúng ta chạy người gửi trên EURUSD, sau đó là người nhận trên XAUUSD. Nhưng lần này chúng ta sẽ chỉ định EnableFlashing=true
cho người gửi.
Đây là những gì xảy ra trong nhật ký:
(EURUSD,H1) *
(EURUSD,H1) FileOpen(dataport,FILE_BIN|FILE_WRITE|FILE_SHARE_READ|flag)=1 / ok
(EURUSD,H1) Written[1]: 1629653638
(XAUUSD,H1) *
(XAUUSD,H1) FileOpen(dataport,FILE_BIN|FILE_WRITE|FILE_SHARE_READ|flag)=-1 / CANNOT_OPEN_FILE(5004)
(XAUUSD,H1) FileOpen(dataport,FILE_BIN|FILE_READ|FILE_SHARE_WRITE|FILE_SHARE_READ|flag)=1 / ok
(XAUUSD,H1) Read[1]: 1629653638 (size=8, before=0(false), after=8)
(EURUSD,H1) Written[2]: 1629653643
(XAUUSD,H1) Read[2]: 1629653643 (size=8, before=8(true), after=16)
(EURUSD,H1) Written[3]: 1629653648
(XAUUSD,H1) Read[3]: 1629653648 (size=8, before=16(true), after=24)
(EURUSD,H1) Written[4]: 1629653653
(XAUUSD,H1) Read[4]: 1629653653 (size=8, before=24(true), after=32)
(EURUSD,H1) Written[5]: 1629653658
2
3
4
5
6
7
8
9
10
11
12
13
14
Cùng một tệp lại được mở thành công ở các chế độ khác nhau trong cả hai tập lệnh, nhưng lần này các giá trị được ghi được người nhận đọc đều đặn.
Điều thú vị cần lưu ý là trước mỗi lần đọc dữ liệu tiếp theo, trừ lần đầu tiên, hàm FileIsEnding
trả về true
(hiển thị trong cùng chuỗi với dữ liệu nhận được, trong dấu ngoặc sau chuỗi "before"). Do đó, có dấu hiệu rằng chúng ta đang ở cuối tệp, nhưng sau đó FileReadLong
đọc thành công một giá trị được cho là ngoài giới hạn tệp và dịch chuyển vị trí sang phải. Ví dụ, mục nhập "size=8, before=8(true), after=16" có nghĩa là kích thước tệp được báo cáo cho chương trình MQL là 8, con trỏ hiện tại trước khi gọi FileReadLong
cũng bằng 8 và dấu hiệu cuối tệp được bật. Sau khi gọi thành công FileReadLong
, con trỏ được di chuyển đến 16. Tuy nhiên, trong lần lặp tiếp theo và tất cả các lần khác, chúng ta lại thấy "size=8", và con trỏ dần dần di chuyển ra xa hơn ngoài tệp.
Vì việc ghi trong người gửi và đọc trong người nhận xảy ra mỗi 5 giây, tùy thuộc vào pha chênh lệch vòng lặp của chúng, chúng ta có thể quan sát hiệu ứng của độ trễ khác nhau giữa hai thao tác, lên đến gần 5 giây trong trường hợp xấu nhất. Tuy nhiên, điều này không có nghĩa là việc xóa bộ đệm chậm như vậy. Trên thực tế, đó là một quá trình gần như tức thời. Để đảm bảo phát hiện thay đổi nhanh hơn, bạn có thể giảm thời gian ngủ trong các vòng lặp (lưu ý rằng bài kiểm tra này, nếu độ trễ quá ngắn, sẽ nhanh chóng làm đầy nhật ký — không giống như một chương trình thực tế, dữ liệu mới luôn được tạo ra ở đây vì đây là thời gian hiện tại của người gửi đến giây gần nhất).
Nhân tiện, bạn có thể chạy nhiều người nhận, trái ngược với người gửi chỉ được có một. Nhật ký dưới đây cho thấy hoạt động của một người gửi trên EURUSD và hai người nhận trên các biểu đồ XAUUSD và USDRUB.
(EURUSD,H1) *
(EURUSD,H1) FileOpen(dataport,FILE_BIN|FILE_WRITE|FILE_SHARE_READ|flag)=1 / ok
(EURUSD,H1) Written[1]: 1629671658
(XAUUSD,H1) *
(XAUUSD,H1) FileOpen(dataport,FILE_BIN|FILE_WRITE|FILE_SHARE_READ|flag)=-1 / CANNOT_OPEN_FILE(5004)
(XAUUSD,H1) FileOpen(dataport,FILE_BIN|FILE_READ|FILE_SHARE_WRITE|FILE_SHARE_READ|flag)=1 / ok
(XAUUSD,H1) Read[1]: 1629671658 (size=8, before=0(false), after=8)
(EURUSD,H1) Written[2]: 1629671663
(USDRUB,H1) *
(USDRUB,H1) FileOpen(dataport,FILE_BIN|FILE_WRITE|FILE_SHARE_READ|flag)=-1 / CANNOT_OPEN_FILE(5004)
(USDRUB,H1) FileOpen(dataport,FILE_BIN|FILE_READ|FILE_SHARE_WRITE|FILE_SHARE_READ|flag)=1 / ok
(USDRUB,H1) Read[1]: 1629671658 (size=16, before=0(false), after=8)
(USDRUB,H1) Read[2]: 1629671663 (size=16, before=8(false), after=16)
(XAUUSD,H1) Read[2]: 1629671663 (size=8, before=8(true), after=16)
(EURUSD,H1) Written[3]: 1629671668
(USDRUB,H1) Read[3]: 1629671668 (size=16, before=16(true), after=24)
(XAUUSD,H1) Read[3]: 1629671668 (size=8, before=16(true), after=24)
(EURUSD,H1) Written[4]: 1629671673
(USDRUB,H1) Read[4]: 1629671673 (size=16, before=24(true), after=32)
(XAUUSD,H1) Read[4]: 1629671673 (size=8, before=24(true), after=32)
(EURUSD,H1) Written[5]: 1629671678
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Đến thời điểm tập lệnh thứ ba trên USDRUB được khởi chạy, đã có 2 bản ghi 8 byte trong tệp, vì vậy vòng lặp bên trong ngay lập tức thực hiện 2 lần lặp từ FileReadLong
, và kích thước tệp "dường như" bằng 16.