2013-08-19 46 views
0

我們有一個使用INDY的Delphi客戶端服務器應用程序。客戶端有一個到多線程服務器的tIdTCPClient連接。客戶是「理論上」的單一線程。但實際上客戶端上有多個線程,這是我的問題所在。例如,想象一下每分鐘觸發一次從服務器獲取數據的計時器。並且考慮一下當用戶在這個計時器事件的同時運行一個命令時會發生什麼。事實上,我的問題是由我們的「報告生成器」報告工具引起的,(令人煩惱地)堅持要加載報告的每一頁,這需要一段時間。該報告運行我們的「特殊」數據集,該數據集具有緩存機制來一次傳輸批量記錄(因此多次調用服務器以獲取所有數據)。同時,如果用戶在同一時間做了其他事情,我們似乎正在越過數據。用戶似乎收回了用於報告的數據。帶有定時器事件和其他多線程客戶端事件的Delphi tIdTCPClient

順便說一句,這個錯誤非常罕見,但對於一個擁有世界上最慢互聯網的特定客戶(運氣 - 我現在有一個測試環境),這種錯誤少得多。

所以在客戶端上我的代碼有點像這樣...

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。

我想我需要使應用程序,以便上述過程一次只能由一個線程運行。其他線程必須等待。

有人請幫忙的語法。

回答

2

TIdIOHandlerWrite()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,或接收在將其複製回TStream之前將其作爲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,您確實需要重新考慮您的整體客戶端設計。

最起碼,使用現有的邏輯,當你需要發送一個命令並讀取響應,停止計時,並等待所有待處理的數據,然後發送命令之前進行交換,而不允許其他任何訪問套接字直到響應返回。您一次嘗試做得太多而沒有同步所有內容以避免重疊。

從長遠來看,從單個專用線程完成所有讀取並將接收到的數據傳遞給其他線程以進行必要的處理會更安全。但是這也意味着要改變發送邏輯以匹配。你既可以:

  1. 如果你的協議可以讓你有在平行飛行多個命令,那麼你可以從任何線程隨時發送一個命令(只是一定要使用的一個關鍵部分,以避免重複) ,但不要馬上等待迴應。讓每個發送線程繼續前進並執行其他操作,並且當預期的響應實際到達時,讓讀線程異步通知相應的發送線程。

  2. 如果協議不允許平行的命令,但你仍然需要在每個發送線程等待其各自的響應,然後給套接字線程一個線程安全的隊列中的其它線程可以把命令需要的時候進入。套接字線程然後可以通過該隊列週期性地發送每個命令並根據需要一次接收一個響應。將命令放入隊列的每個線程都可以包含一個TEvent,以便在響應到達時用信號發送,這樣,它們可以在等待時進入高效的睡眠狀態,但可以保留每線程的等待邏輯。

0

謝謝雷米。

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;