Hiện tại tôi đang làm việc trên ứng dụng khách/máy chủ Delphi XE3 để chuyển tệp (với các thành phần Indy FTP). Phần khách hàng theo dõi một thư mục, nhận danh sách các tệp bên trong, tải chúng lên máy chủ và xóa các tệp gốc. Việc tải lên được thực hiện bằng một chuỗi riêng biệt, xử lý từng tệp một. Các tệp có thể nằm trong khoảng từ 0 đến vài nghìn và kích thước của chúng cũng thay đổi rất nhiều.Đồng bộ hóa tệp tải lên đa luồng
Đây là một ứng dụng Firemonkey được biên dịch cho cả OSX và Windows, vì vậy tôi đã phải sử dụng TThread thay vì OmniThreadLibrary, mà tôi ưa thích. Khách hàng của tôi báo cáo rằng ứng dụng bị đóng băng ngẫu nhiên. Tôi không thể sao chép nó, nhưng vì tôi không có nhiều kinh nghiệm với TThread, tôi có thể đã đặt tình trạng bế tắc ở đâu đó. Tôi đọc khá nhiều ví dụ, nhưng tôi vẫn không chắc chắn về một số chi tiết đa luồng.
Cấu trúc ứng dụng rất đơn giản:
Bộ hẹn giờ trong chuỗi chính sẽ kiểm tra thư mục và nhận thông tin về từng tệp vào một bản ghi, đi vào TList chung. Danh sách này giữ thông tin về tên của các tệp, kích thước, tiến trình, cho dù tệp được tải lên hoàn toàn hay phải được thử lại. Tất cả được hiển thị trong một lưới với thanh tiến trình, vv Danh sách này chỉ được truy cập bởi chuỗi chính. Sau đó các mục từ danh sách được gửi đến luồng bằng cách gọi phương thức AddFile (mã bên dưới). Chuỗi lưu trữ tất cả các tệp trong một hàng đợi an toàn theo chủ đề như thế này http://delphihaven.wordpress.com/2011/05/06/using-tmonitor-2/
Khi tệp được tải lên, chuỗi trình tải lên sẽ thông báo chuỗi chính với cuộc gọi đến Đồng bộ hóa.
Chuỗi chính định kỳ gọi phương thức Uploader.GetProgress để kiểm tra tiến trình của tệp hiện tại và hiển thị nó. Chức năng này không thực sự an toàn với luồng, nhưng nó có thể gây ra bế tắc hay chỉ trả về dữ liệu sai?
Cách an toàn và hiệu quả để thực hiện kiểm tra tiến độ là gì?
Vì vậy, cách tiếp cận này là OK hoặc tôi đã bỏ lỡ điều gì đó? Bạn sẽ làm điều này như thế nào?
Ví dụ: tôi mặc dù tạo chuỗi mới chỉ để đọc nội dung thư mục. Điều này có nghĩa là TList tôi sử dụng phải được tạo luồng an toàn, nhưng nó phải được truy cập mọi lúc để làm mới thông tin được hiển thị trong lưới GUI. Sẽ không phải tất cả việc đồng bộ hóa chỉ làm chậm GUI?
Tôi đã đăng mã đơn giản dưới đây trong trường hợp ai đó muốn xem nó. Nếu không, tôi sẽ rất vui khi nghe một số ý kiến về những gì tôi nên sử dụng nói chung. Các mục tiêu chính là làm việc trên cả OSX và Windows; để có thể hiển thị thông tin về tất cả các tệp và tiến trình của tệp hiện tại; và đáp ứng bất kể số lượng và kích thước của tệp.
Đó là mã của chuỗi trình tải lên. Tôi đã xóa một số của nó cho việc đọc dễ dàng hơn:
type
TFileStatus = (fsToBeQueued, fsUploaded, fsQueued);
TFileInfo = record
ID: Integer;
Path: String;
Size: Int64;
UploadedSize: Int64;
Status: TFileStatus;
end;
TUploader = class(TThread)
private
FTP: TIdFTP;
fQueue: TThreadedQueue<TFileInfo>;
fCurrentFile: TFileInfo;
FUploading: Boolean;
procedure ConnectFTP;
function UploadFile(aFileInfo: TFileInfo): String;
procedure OnFTPWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
procedure SignalComplete;
procedure SignalError(aError: String);
protected
procedure Execute; override;
public
property Uploading: Boolean read FUploading;
constructor Create;
destructor Destroy; override;
procedure Terminate;
procedure AddFile(const aFileInfo: TFileInfo);
function GetProgress: TFileInfo;
end;
procedure TUploader.AddFile(const aFileInfo: TFileInfo);
begin
fQueue.Enqueue(aFileInfo);
end;
procedure TUploader.ConnectFTP;
begin
...
FTP.Connect;
end;
constructor TUploader.Create;
begin
inherited Create(false);
FreeOnTerminate := false;
fQueue := TThreadedQueue<TFileInfo>.Create;
// Create the TIdFTP and set ports and other params
...
end;
destructor TUploader.Destroy;
begin
fQueue.Close;
fQueue.Free;
FTP.Free;
inherited;
end;
// Process the whole queue and inform the main thread of the progress
procedure TUploader.Execute;
var
Temp: TFileInfo;
begin
try
ConnectFTP;
except
on E: Exception do
SignalError(E.Message);
end;
// Use Peek instead of Dequeue, because the item should not be removed from the queue if it fails
while fQueue.Peek(fCurrentFile) = wrSignaled do
try
if UploadFile(fCurrentFile) = '' then
begin
fQueue.Dequeue(Temp); // Delete the item from the queue if succesful
SignalComplete;
end;
except
on E: Exception do
SignalError(E.Message);
end;
end;
// Return the current file's info to the main thread. Used to update the progress indicators
function TUploader.GetProgress: TFileInfo;
begin
Result := fCurrentFile;
end;
// Update the uploaded size for the current file. This information is retrieved by a timer from the main thread to update the progress bar
procedure TUploader.OnFTPWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64);
begin
fCurrentFile.UploadedSize := AWorkCount;
end;
procedure TUploader.SignalComplete;
begin
Synchronize(
procedure
begin
frmClientMain.OnCompleteFile(fCurrentFile);
end);
end;
procedure TUploader.SignalError(aError: String);
begin
try
FTP.Disconnect;
except
end;
if fQueue.Closed then
Exit;
Synchronize(
procedure
begin
frmClientMain.OnUploadError(aError);
end);
end;
// Clear the queue and terminate the thread
procedure TUploader.Terminate;
begin
fQueue.Close;
inherited;
end;
function TUploader.UploadFile(aFileInfo: TFileInfo): String;
begin
Result := 'Error';
try
if not FTP.Connected then
ConnectFTP;
FUploading := true;
FTP.Put(aFileInfo.Path, ExtractFileName(aFileInfo.Path));
Result := '';
finally
FUploading := false;
end;
end;
Và các bộ phận của các chủ đề chính mà tương tác với người tải lên:
......
// Main form
fUniqueID: Integer; // This is a unique number given to each file, because there might be several with the same names(after one is uploaded and deleted)
fUploader: TUploader; // The uploader thread
fFiles: TList<TFileInfo>;
fCurrentFileName: String; // Used to display the progress
function IndexOfFile(aID: Integer): Integer; //Return the index of the record inside the fFiles given the file ID
public
procedure OnCompleteFile(aFileInfo: TFileInfo);
procedure OnUploadError(aError: String);
end;
// This is called by the uploader with Synchronize
procedure TfrmClientMain.OnUploadError(aError: String);
begin
// show and log the error
end;
// This is called by the uploader with Synchronize
procedure TfrmClientMain.OnCompleteFile(aFileInfo: TFileInfo);
var
I: Integer;
begin
I := IndexOfFile(aFileInfo.ID);
if (I >= 0) and (I < fFiles.Count) then
begin
aFileInfo.Status := fsUploaded;
aFileInfo.UploadedSize := aFileInfo.Size;
FFiles.Items[I] := aFileInfo;
Inc(FFilesUploaded);
TFile.Delete(aFileInfo.Path);
colProgressImg.UpdateCell(I);
end;
end;
procedure TfrmClientMain.ProcessFolder;
var
NewFiles: TStringDynArray;
I, J: Integer;
FileInfo: TFileInfo;
begin
// Remove completed files from the list if it contains more than XX files
while FFiles.Count > 1000 do
if FFiles[0].Status = fsUploaded then
begin
Dec(FFilesUploaded);
FFiles.Delete(0);
end else
Break;
NewFiles := TDirectory.GetFiles(WatchFolder, '*.*',TSearchOption.soAllDirectories);
for I := 0 to Length(NewFiles) - 1 do
begin
FileInfo.ID := FUniqueID;
Inc(FUniqueID);
FileInfo.Path := NewFiles[I];
FileInfo.Size := GetFileSizeByName(NewFiles[I]);
FileInfo.UploadedSize := 0;
FileInfo.Status := fsToBeQueued;
FFiles.Add(FileInfo);
if (I mod 100) = 0 then
begin
UpdateStatusLabel;
grFiles.RowCount := FFiles.Count;
Application.ProcessMessages;
if fUploader = nil then
break;
end;
end;
// Send the new files and resend failed to the uploader thread
for I := 0 to FFiles.Count - 1 do
if (FFiles[I].Status = fsToBeQueued) then
begin
if fUploader = nil then
Break;
FileInfo := FFiles[I];
FileInfo.Status := fsQueued;
FFiles[I] := FileInfo;
SaveDebug(1, 'Add: ' + ExtractFileName(FFiles[I].Path));
FUploader.AddFile(FFiles[I]);
end;
end;
procedure TfrmClientMain.tmrGUITimer(Sender: TObject);
var
FileInfo: TFileInfo;
I: Integer;
begin
if (fUploader = nil) or not fUploader.Uploading then
Exit;
FileInfo := fUploader.GetProgress;
I := IndexOfFile(FileInfo.ID);
if (I >= 0) and (I < fFiles.Count) then
begin
fFiles.Items[I] := FileInfo;
fCurrentFileName := ExtractFileName(FileInfo.Path);
colProgressImg.UpdateCell(I);
end;
end;
function TfrmClientMain.IndexOfFile(aID: Integer): Integer;
var
I: Integer;
begin
Result := -1;
for I := 0 to FFiles.Count - 1 do
if FFiles[I].ID = aID then
Exit(I);
end;
Tôi không chắc chắn và chưa thử nghiệm .. nhưng bạn đã cố gắng thêm TIdAntiFreeze và kiểm tra xem hành vi có giống nhau hay không? (FMX.IdAntiFreeze) – Whiler
TIdAntiFreeze được thiết kế để ngăn chặn sự đóng băng của GUI khi bạn sử dụng một thành phần Indy từ sợi chính (ví dụ: bị bỏ trên biểu mẫu). Tôi sử dụng nó trong một chủ đề riêng biệt vì vậy tôi không thấy nó sẽ giúp ích gì. Ít nhất theo như tôi biết ... – VGeorgiev
Ở giao diện đầu tiên, xử lý lỗi của bạn có vẻ sai với tôi. Ví dụ, trong phương thức Execute, nếu cuộc gọi ConnectFTP thất bại, bạn _eat_ ngoại lệ (sau khi thông báo về lỗi), và bạn vẫn phát hành các cuộc gọi đến UploadFile. IMHO bạn phải _clean_ đó, và để cho thread chết với một FatalException hoặc xử lý đúng ngoại lệ bên trong phương thức Execute, ví dụ, thử lại kết nối một số lần, có thể phụ thuộc vào loại lỗi. Mặt khác, nếu bạn có danh sách trong chuỗi chính, tôi không thấy lý do tại sao bạn cần một hàng đợi trong từng chuỗi riêng lẻ. – jachguate