2010-03-25 52 views
14

Tôi thỉnh thoảng gặp vấn đề với bế tắc khi phá hủy một số chủ đề. Tôi đã cố gắng để gỡ lỗi vấn đề nhưng bế tắc dường như không tồn tại khi gỡ lỗi trong IDE, có lẽ vì tốc độ thấp của các sự kiện trong IDE.Delphi threads bế tắc

Sự cố:

Chủ đề chính tạo ra một số chủ đề khi ứng dụng bắt đầu. Các chủ đề luôn luôn sống và đồng bộ hóa với chủ đề chính. Không sao cả. Các chủ đề bị hủy khi ứng dụng kết thúc (mainform.onclose) như thế này:

thread1.terminate; 
thread1.waitfor; 
thread1.free; 

v.v.

Nhưng đôi khi một trong các chuỗi (ghi nhật ký một số chuỗi vào bản ghi nhớ, sử dụng đồng bộ hóa) sẽ khóa toàn bộ ứng dụng khi đóng. Tôi nghi ngờ rằng thread là đồng bộ khi tôi gọi waitform và harmaggeddon xảy ra, nhưng đó chỉ là một đoán vì bế tắc không bao giờ xảy ra khi gỡ lỗi (hoặc tôi chưa bao giờ có thể tái tạo nó anyway). Lời khuyên nào?

+1

Mã sẽ hữu ích, ít nhất là với cuộc gọi của bạn để Đồng bộ hóa và xóa sạch chuỗi vi phạm. –

Trả lời

27

Thông báo ghi nhật ký chỉ là một trong những khu vực mà ở đó Synchronize() không có ý nghĩa gì cả. Thay vào đó, bạn nên tạo một đối tượng mục tiêu nhật ký, trong đó có một danh sách chuỗi, được bảo vệ bởi một phần quan trọng và thêm các thông điệp tường trình của bạn vào đó. Có chủ đề VCL chính loại bỏ các thông điệp tường trình từ danh sách đó và hiển thị chúng trong cửa sổ nhật ký. Này có một số ưu điểm:

  • Bạn không cần phải gọi Synchronize(), mà chỉ là một ý tưởng tồi. Hiệu ứng phụ tốt đẹp là loại vấn đề tắt máy của bạn biến mất.

  • Chủ đề người lao động có thể tiếp tục công việc của mình mà không bị chặn xử lý sự kiện chủ đề chính hoặc trên các chủ đề khác đang cố đăng nhập thư.

  • Tăng hiệu suất, vì nhiều thư có thể được thêm vào cửa sổ nhật ký trong một lần. Nếu bạn sử dụng BeginUpdate()EndUpdate() điều này sẽ tăng tốc độ.

Không có bất lợi nào mà tôi có thể thấy - thứ tự của thông điệp tường trình cũng được giữ nguyên.

Edit:

tôi sẽ bổ sung thêm một số chi tiết thông tin và một chút mã để chơi với, để minh họa rằng có những cách tốt hơn để làm những gì bạn cần phải làm.

Gọi Synchronize() từ một luồng khác với chuỗi ứng dụng chính trong chương trình VCL sẽ làm cho chuỗi gọi bị chặn, mã được truyền sẽ được thực hiện trong ngữ cảnh của chuỗi VCL, và sau đó chuỗi gọi sẽ được bỏ chặn và tiếp tục chạy. Đó có thể là một ý tưởng hay trong thời gian của các máy xử lý đơn lẻ, mà chỉ có một luồng có thể chạy cùng lúc, nhưng với nhiều bộ xử lý hoặc lõi thì đó là một sự lãng phí khổng lồ và nên tránh bằng mọi giá. Nếu bạn có 8 chủ đề công nhân trên một máy 8 lõi, khi họ gọi Synchronize() có thể sẽ giới hạn thông lượng tới một phần nhỏ của những gì có thể.

Thực ra, gọi Synchronize() không bao giờ là ý tưởng hay vì nó có thể dẫn đến deadlocks. Một lý do thuyết phục hơn để không sử dụng nó, bao giờ hết.

Sử dụng PostMessage() để gửi các thông điệp log sẽ chăm sóc của vấn đề bế tắc, nhưng nó có vấn đề riêng của mình:

  • Mỗi chuỗi log sẽ gây ra một thông điệp tới được đăng tải và xử lý, gây ra nhiều chi phí. Không có cách nào để xử lý một số thông điệp tường trình trong một lần.

  • Thông báo của Windows chỉ có thể mang dữ liệu có kích thước bằng máy trong các tham số. Do đó, việc gửi chuỗi là không thể. Việc gửi các chuỗi sau khi nhập một mẫu đến PChar là không an toàn, vì chuỗi có thể đã được giải phóng vào lúc thông báo được xử lý. Phân bổ bộ nhớ trong luồng công nhân và giải phóng bộ nhớ đó trong chuỗi VCL sau khi thông báo đã được xử lý là một lối thoát. Một cách làm tăng thêm chi phí.

  • Hàng đợi thư trong Windows có kích thước hữu hạn. Đăng quá nhiều thư có thể dẫn đến hàng đợi trở nên đầy đủ và thư bị xóa. Đó không phải là một điều tốt, và cùng với điểm trước đó nó dẫn đến rò rỉ bộ nhớ.

  • Tất cả thư trong hàng đợi sẽ được xử lý trước khi bất kỳ bộ đếm thời gian hoặc tin nhắn vẽ nào sẽ được tạo. Một luồng ổn định của nhiều tin nhắn được đăng có thể khiến chương trình trở nên không phản hồi.

Một cấu trúc dữ liệu thu thập các thông điệp đăng nhập có thể trông như thế này:

type 
    TLogTarget = class(TObject) 
    private 
    fCritSect: TCriticalSection; 
    fMsgs: TStrings; 
    public 
    constructor Create; 
    destructor Destroy; override; 

    procedure GetLoggedMsgs(AMsgs: TStrings); 
    procedure LogMessage(const AMsg: string); 
    end; 

constructor TLogTarget.Create; 
begin 
    inherited; 
    fCritSect := TCriticalSection.Create; 
    fMsgs := TStringList.Create; 
end; 

destructor TLogTarget.Destroy; 
begin 
    fMsgs.Free; 
    fCritSect.Free; 
    inherited; 
end; 

procedure TLogTarget.GetLoggedMsgs(AMsgs: TStrings); 
begin 
    if AMsgs <> nil then begin 
    fCritSect.Enter; 
    try 
     AMsgs.Assign(fMsgs); 
     fMsgs.Clear; 
    finally 
     fCritSect.Leave; 
    end; 
    end; 
end; 

procedure TLogTarget.LogMessage(const AMsg: string); 
begin 
    fCritSect.Enter; 
    try 
    fMsgs.Add(AMsg); 
    finally 
    fCritSect.Leave; 
    end; 
end; 

Nhiều chủ đề có thể gọi LogMessage() đồng thời, vào cấu hình phần quan trọng sẽ serialize truy cập vào danh sách, và sau khi thêm thông điệp của họ các chủ đề có thể tiếp tục với công việc của họ.

Điều đó để lại câu hỏi cách chuỗi VCL biết khi nào cần gọi GetLoggedMsgs() để xóa thư khỏi đối tượng và thêm chúng vào cửa sổ. Một phiên bản của người nghèo sẽ có một bộ đếm thời gian và bình chọn. Một cách tốt hơn là gọi PostMessage() khi thông điệp tường trình được thêm vào:

procedure TLogTarget.LogMessage(const AMsg: string); 
begin 
    fCritSect.Enter; 
    try 
    fMsgs.Add(AMsg); 
    PostMessage(fNotificationHandle, WM_USER, 0, 0); 
    finally 
    fCritSect.Leave; 
    end; 
end; 

Điều này vẫn gặp sự cố với quá nhiều thư được đăng.Chỉ cần đăng thông báo khi tin nhắn trước đó đã được xử lý:

procedure TLogTarget.LogMessage(const AMsg: string); 
begin 
    fCritSect.Enter; 
    try 
    fMsgs.Add(AMsg); 
    if InterlockedExchange(fMessagePosted, 1) = 0 then 
     PostMessage(fNotificationHandle, WM_USER, 0, 0); 
    finally 
    fCritSect.Leave; 
    end; 
end; 

Điều đó vẫn có thể được cải thiện. Sử dụng bộ hẹn giờ giải quyết được vấn đề của các tin nhắn đã đăng tải sẽ lấp đầy hàng đợi. Sau đây là một lớp học nhỏ mà thực hiện điều này:

type 
    TMainThreadNotification = class(TObject) 
    private 
    fNotificationMsg: Cardinal; 
    fNotificationRequest: integer; 
    fNotificationWnd: HWND; 
    fOnNotify: TNotifyEvent; 
    procedure DoNotify; 
    procedure NotificationWndMethod(var AMsg: TMessage); 
    public 
    constructor Create; 
    destructor Destroy; override; 

    procedure RequestNotification; 
    public 
    property OnNotify: TNotifyEvent read fOnNotify write fOnNotify; 
    end; 

constructor TMainThreadNotification.Create; 
begin 
    inherited Create; 
    fNotificationMsg := RegisterWindowMessage('thrd_notification_msg'); 
    fNotificationRequest := -1; 
    fNotificationWnd := AllocateHWnd(NotificationWndMethod); 
end; 

destructor TMainThreadNotification.Destroy; 
begin 
    if IsWindow(fNotificationWnd) then 
    DeallocateHWnd(fNotificationWnd); 
    inherited Destroy; 
end; 

procedure TMainThreadNotification.DoNotify; 
begin 
    if Assigned(fOnNotify) then 
    fOnNotify(Self); 
end; 

procedure TMainThreadNotification.NotificationWndMethod(var AMsg: TMessage); 
begin 
    if AMsg.Msg = fNotificationMsg then begin 
    SetTimer(fNotificationWnd, 42, 10, nil); 
    // set to 0, so no new message will be posted 
    InterlockedExchange(fNotificationRequest, 0); 
    DoNotify; 
    AMsg.Result := 1; 
    end else if AMsg.Msg = WM_TIMER then begin 
    if InterlockedExchange(fNotificationRequest, 0) = 0 then begin 
     // set to -1, so new message can be posted 
     InterlockedExchange(fNotificationRequest, -1); 
     // and kill timer 
     KillTimer(fNotificationWnd, 42); 
    end else begin 
     // new notifications have been requested - keep timer enabled 
     DoNotify; 
    end; 
    AMsg.Result := 1; 
    end else begin 
    with AMsg do 
     Result := DefWindowProc(fNotificationWnd, Msg, WParam, LParam); 
    end; 
end; 

procedure TMainThreadNotification.RequestNotification; 
begin 
    if IsWindow(fNotificationWnd) then begin 
    if InterlockedIncrement(fNotificationRequest) = 0 then 
    PostMessage(fNotificationWnd, fNotificationMsg, 0, 0); 
    end; 
end; 

Một thể hiện của lớp có thể được thêm vào TLogTarget, để gọi một sự kiện thông báo trong các chủ đề chính, nhưng thời gian tối đa là vài chục mỗi giây.

+1

+ NUM_ALLOWED_VOTES :) đây là một bài viết tuyệt vời! – jpfollenius

+0

Khi nào tôi nên gọi 'RequestNotification'? – EProgrammerNotFound

+0

@MatheusFreitas: Bất cứ khi nào một chủ đề không phải là chủ đề chính cần chủ đề chính để làm điều gì đó trong ngữ cảnh của nó. Trong trường hợp của hệ thống ghi nhật ký, một thông điệp tường trình được thêm vào trong một chuỗi công nhân cần được hiển thị trong GUI. Nếu chuỗi công việc gọi 'RequestNotification()' thì nó có thể tiếp tục công việc của nó trong khi chắc chắn rằng một thời gian trong tương lai luồng GUI sẽ được thông báo về thông điệp tường trình mới và hiển thị nó. Tôi hy vọng điều đó đủ rõ ràng? – mghie

2

Thêm đối tượng mutex vào chuỗi chính. Nhận mutex khi thử biểu mẫu đóng. Trong thread khác kiểm tra mutex trước khi đồng bộ hóa trong trình tự xử lý.

7

Cân nhắc thay thế Synchronize bằng một cuộc gọi đến PostMessage và xử lý thông báo này trong biểu mẫu để thêm thông báo tường trình vào ghi nhớ. Một cái gì đó dọc theo dòng: (lấy nó như là mã giả)

WM_LOG = WM_USER + 1; 
... 
MyForm = class (TForm) 
    procedure LogHandler (var Msg : Tmessage); message WM_LOG; 
end; 
... 
PostMessage (Application.MainForm.Handle, WM_LOG, 0, PChar (LogStr)); 

Điều đó tránh được tất cả các vấn đề bế tắc của hai luồng đang chờ nhau.

EDIT (Cảm ơn Serg cho gợi ý): Lưu ý rằng việc truyền chuỗi theo cách được mô tả không an toàn vì chuỗi có thể bị hủy trước khi chủ đề VCL sử dụng. Như tôi đã đề cập - điều này chỉ được dự định là mã giả.

+1

Sai. 'SendMessage()' sẽ chặn giống nhau nếu vòng lặp tin nhắn của chuỗi nhận sẽ không xử lý thông báo, như khi nó nằm trong 'WaitForSingleObject()', đợi cho chuỗi kết thúc. – mghie

+1

Bạn nên sử dụng PostMessage thay vì SendMessage để tránh chặn luồng. – kludg

+0

Phải, tôi bối rối cả hai. Cảm ơn @Serg vì những lời chỉ trích mang tính xây dựng hơn. – jpfollenius

1

Rất đơn giản:

TMyThread = class(TThread) 
protected 
    FIsIdle: boolean; 
    procedure Execute; override; 
    procedure MyMethod; 
public 
    property IsIdle : boolean read FIsIdle write FIsIdle; //you should use critical section to read/write it 
end; 

procedure TMyThread.Execute; 
begin 
    try 
    while not Terminated do 
    begin 
     Synchronize(MyMethod); 
     Sleep(100); 
    end; 
    finally 
    IsIdle := true; 
    end; 
end; 

//thread destroy; 
lMyThread.Terminate; 
while not lMyThread.IsIdle do 
begin 
    CheckSynchronize; 
    Sleep(50); 
end; 
+1

Không cần sử dụng các phần quan trọng cho boolean được viết chỉ một lần. – mghie

0

đối tượng TThread Delphi (và các lớp kế thừa) đã kêu gọi WAITFOR khi phá hủy, nhưng nó phụ thuộc vào việc bạn tạo ra các chủ đề với CreateSuspended hay không. Nếu bạn đang sử dụng CreateSuspended = true để thực hiện khởi tạo thêm trước khi gọi Resume đầu tiên, bạn nên xem xét việc tạo hàm tạo của riêng bạn (gọi inherited Create(false);) để thực hiện khởi tạo thêm.

Các vấn đề liên quan