[英]Delphi tIdTCPClient with timer events and other multi-threaded client side events
我們有一個使用INDY的Delphi客戶端服務器應用程序。 客戶端具有到服務器的單個tIdTCPClient連接,該連接是多線程的。 客戶端“理論上”是一個單一的線程。 但實際上客戶端上有多個線程,這就是我的問題所在。 例如,想一下每分鍾觸發一次從服務器獲取數據的計時器。 並考慮當用戶在此計時器事件的同時運行命令時會發生什么。 事實上,我的問題是由我們的“報表生成器”報表工具引起的(令人討厭)堅持加載報表的每一頁,這需要一段時間。 該報告運行我們的“特殊”數據集,該數據集具有一次傳輸批量記錄的緩存機制(因此多次調用服務器以獲取所有數據)。 同時,如果用戶同時做了其他事情,我們似乎正在獲取交叉數據。 用戶似乎正在獲取用於報告的數據。
順便說一句,這個bug是非常罕見的,但對於擁有世界上最慢的互聯網的一個特定客戶來說,這種情況很少見(祝我好運 - 我現在有一個測試環境)。
所以在客戶端我的代碼有點像這樣......
procedure DoCommand(MyIdTCPClient:tIdTCPClient; var DATA:tMemoryStream);
var
Buffer: TBytes;
DataSize: Integer;
CommsVerTest: String;
begin
//Write Data
MyIdTCPClient.IOHandler.Write(DATA.Size);
MyIdTCPClient.IOHandler.Write(RawToBytes(Data.Memory^,DataSize));
//Read back 6 bytes CommsVerTest should always be the same (ie ABC123)
SetLength(Buffer,0); //Clear out buffer
MyIdTCPClient.IOHandler.ReadBytes(Buffer,6);
CommsVerTest:=BytesToString(Buffer);
if CommsVerTest<>'ABC123' then
raise exception.create('Invalid Comms'); //It bugs out here in rare cases
//Get Result Data Back from Server
DataSize:=MyIdTCPClient.IOHandler.ReadLongInt;
Data.SetSize(DataSize); //Report thread is stuck here
MyIdTCPClient.IOHandler.ReadBytes(Buffer,DataSize);
end;
現在,當我調試它時,我可以在此過程中間有兩個線程時確認它是錯誤的。 主線程在異常處停止。 並且報告線程在相同的過程中被卡在其他位置。
因此,在我看來,我需要使上面的過程安全。 我的意思是,如果用戶想要做某事,他們只需要等到報告線程結束。
Arrrgh,我以為我的客戶端應用程序是單線程的,用於向服務器發送數據!
我認為使用TThread是行不通的 - 因為我無法訪問Report Builder中的線程。 我想我需要一個tCriticalSection。
我想我需要創建應用程序,以便上述過程一次只能由一個線程運行。 其他線程必須等待。
有人請幫助解決語法問題。
TIdIOHandler
具有Write()
和Read...()
重載,用於發送/接收TStream
數據:
procedure Write(AStream: TStream; ASize: TIdStreamSize = 0; AWriteByteCount: Boolean = False); overload; virtual;
procedure ReadStream(AStream: TStream; AByteCount: TIdStreamSize = -1; AReadUntilDisconnect: Boolean = False); virtual;
您無需在發送之前將TMemoryStream
內容復制到中間TIdBytes
,或者在將其復制回TIdBytes
之前將其作為TIdBytes
接收。 事實上,您所展示的代碼中沒有任何內容需要直接使用TIdBytes
:
procedure DoCommand(MyIdTCPClient: TIdTCPClient; var DATA: TMemoryStream);
var
CommsVerTest: String;
begin
//Write Data
MyIdTCPClient.IOHandler.Write(DATA, 0, True);
//Read back 6 bytes CommsVerTest should always be the same (ie ABC123)
CommsVerTest := MyIdTCPClient.IOHandler.ReadString(6);
if CommsVerTest <> 'ABC123' then
raise exception.create('Invalid Comms');
//Get Result Data Back from Server
DATA.Clear;
MyIdTCPClient.IOHandler.ReadStream(DATA, -1, False);
end;
話雖如此,如果你有多個線程同時寫入同一個套接字,或者多個線程同時從同一個套接字讀取,它們將破壞彼此的數據(或者更糟)。 您需要同步對套接字的訪問權限,例如至少對臨界區進行訪問。 由於您使用TIdTCPClient
多線程,因此您需要重新考慮整體客戶端設計。
至少,使用現有的邏輯,當您需要發送命令並讀取響應時,停止計時器並等待任何待處理的數據交換,然后再發送命令,並且不允許任何其他任何東西訪問套接字直到回復回來。 你試圖一次做太多而沒有同步一切,以避免重疊。
從長遠來看,從單個專用線程執行所有讀取然后將任何接收到的數據傳遞到其他線程以便根據需要進行處理會更安全。 但這也意味着改變你的發送邏輯以匹配。 你可以:
如果您的協議允許您並行運行多個命令,那么您可以隨時從任何線程發送命令(只需確保使用關鍵部分以避免重疊),但不要立即等待響應。 讓每個發送線程繼續運行並執行其他操作,並讓讀取線程在預期響應實際到達時異步通知相應的發送線程。
如果協議不允許並行命令,但仍需要每個發送線程等待其各自的響應,那么為套接字線程提供一個線程安全的隊列,其他線程可以在需要時將命令推入其中。 然后,套接字線程可以運行該隊列,根據需要定期發送每個命令並一次接收一個響應。 將命令放入隊列的每個線程可以包括在響應到達時要發信號通知的TEvent
,這樣它們在等待時進入有效的睡眠狀態,但是保留了每線程等待邏輯。
謝謝雷米。
TCriticalSection解決了這個問題。 我無法控制第三方報表生成器等內容。 完全在他們自己的線程中運行報告不會有太大的區別 - 他們仍然需要共享相同的連接(我不想要或不需要並行連接)。 無論如何,程序的大部分都在主線程中運行,並且兩個線程很少需要同時與服務器通信。
所以TCriticalSection是完美的 - 它阻止了這個過程同時運行兩次(即一個線程必須等到第一個完成)。 幸福的是 - 它的工作非常出色。
基本上代碼現在看起來像這樣:
procedure DoCommand(
CS:tCriticalSection;
MyIdTCPClient:tIdTCPClient;
var DATA:tMemoryStream);
var
Buffer: TBytes;
DataSize: Integer;
CommsVerTest: String;
begin
CS.Enter; //enter Critical Section
try
//Write Data
MyIdTCPClient.IOHandler.Write(DATA.Size);
MyIdTCPClient.IOHandler.Write(RawToBytes(Data.Memory^,DataSize));
//Read back 6 bytes CommsVerTest should always be the same (ie ABC123)
SetLength(Buffer,0); //Clear out buffer
MyIdTCPClient.IOHandler.ReadBytes(Buffer,6);
CommsVerTest:=BytesToString(Buffer);
if CommsVerTest<>'ABC123' then
raise exception.create('Invalid Comms');
//Get Result Data Back from Server
DataSize:=MyIdTCPClient.IOHandler.ReadLongInt;
Data.SetSize(DataSize);
MyIdTCPClient.IOHandler.ReadBytes(Buffer,DataSize);
finally
cs.Leave;
end;
end;
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.