2009-11-27 14 views
21

Tôi có một ứng dụng gui VCL đa luồng khá đơn giản được viết bằng Delphi 2007. Tôi thực hiện một số xử lý trong nhiều chủ đề con (tối đa 16 đồng thời) cần cập nhật điều khiển lưới trên biểu mẫu chính của tôi (chỉ cần đăng chuỗi vào lưới)). Không có chủ đề con nào nói chuyện với nhau.Có tốt hơn khi sử dụng "Đồng bộ hóa" của TThread hay sử dụng thông điệp cửa sổ cho IPC giữa chủ đề chính và chuỗi con?

Thiết kế ban đầu của tôi liên quan đến việc gọi TThread's "Synchronize" để cập nhật biểu mẫu kiểm soát lưới trong chuỗi hiện đang chạy. Tuy nhiên, tôi hiểu rằng việc gọi Synchronise về cơ bản thực thi như thể nó là luồng chính khi được gọi. Với tối đa 16 luồng chạy cùng một lúc (và hầu hết quá trình xử lý của chuỗi con sẽ mất từ ​​< 1 giây đến ~ 10 giây), Window Messages có phải là thiết kế tốt hơn không?

Tôi đã làm cho nó hoạt động tại thời điểm mà chủ đề con đăng thông điệp cửa sổ (bao gồm một bản ghi nhiều chuỗi) và chuỗi chính có trình nghe và chỉ cập nhật lưới khi nhận được tin nhắn.

Bất kỳ ý kiến ​​nào về phương pháp tốt nhất cho IPC trong tình huống này? Thông báo cửa sổ hoặc 'Đồng bộ hóa'?

Nếu tôi sử dụng thông báo cửa sổ, bạn có đề xuất đóng gói mã nơi tôi đăng lên lưới trong khối TCriticalSection (nhập và thoát) không? Hoặc tôi sẽ không cần phải lo lắng về an toàn luồng vì tôi đang viết vào lưới trong chuỗi chính (mặc dù trong chức năng của trình xử lý tin nhắn cửa sổ)?

Trả lời

36

Edit:

Dường như rất nhiều các chi tiết thực hiện đã thay đổi kể từ Delphi 4 và 5 (các phiên bản Delphi tôi vẫn đang sử dụng cho hầu hết các công việc của tôi), và Allen Bauer đã comment sau :

Kể từ D6, TThread không sử dụng SendMessage nữa. Nó sử dụng một hàng đợi công việc an toàn thread nơi "công việc" dành cho chủ đề chính được đặt. Một thông báo được đăng lên luồng chính để chỉ ra rằng công việc có sẵn và các chuỗi nền sẽ chặn trên một sự kiện. Khi vòng lặp tin nhắn chính sắp ngừng hoạt động, nó sẽ gọi "CheckSynchronize" để xem có công việc nào đang chờ hay không. Nếu vậy, nó xử lý nó. Khi một mục công việc được hoàn thành, sự kiện mà chủ đề nền bị chặn được thiết lập để cho biết hoàn thành. Được giới thiệu trong khung thời gian D2006, phương thức TThread.Queue đã được thêm vào không chặn.

Cảm ơn bạn đã sửa. Vì vậy, lấy các chi tiết trong câu trả lời ban đầu với một hạt muối.

Nhưng điều này thực sự không ảnh hưởng đến các điểm cốt lõi. Tôi vẫn duy trì rằng toàn bộ ý tưởng của Synchronize() là thiếu sót nghiêm trọng, và điều này sẽ được rõ ràng thời điểm một cố gắng để giữ cho một số lõi của một máy hiện đại chiếm đóng. Đừng "đồng bộ hóa" chủ đề của bạn, hãy để chúng hoạt động cho đến khi chúng được hoàn thành. Cố gắng giảm thiểu tất cả các phụ thuộc giữa chúng. Đặc biệt khi cập nhật GUI có hoàn toàn không lý do để đợi quá trình này hoàn tất. Cho dù Synchronize() sử dụng SendMessage() hoặc PostMessage(), khối đường kết quả là như nhau.


gì bạn trình bày ở đây không phải là một sự thay thế ở tất cả, như Synchronize() sử dụng SendMessage() nội bộ. Vì vậy, nó là nhiều hơn một câu hỏi mà vũ khí bạn muốn sử dụng để bắn mình vào chân với.

Synchronize() đã đi cùng với chúng tôi kể từ khi giới thiệu TThread trong VCL Delphi 2, thật đáng tiếc vì đây là một trong những tính năng thiết kế lớn hơn trong VCL.

Tính năng này hoạt động như thế nào? Nó sử dụng một cuộc gọi SendMessage() đến một cửa sổ đã được tạo trong chủ đề chính, và thiết lập các tham số thông báo để truyền địa chỉ của một phương thức đối tượng parameterless được gọi. Vì các thông điệp Windows sẽ chỉ được xử lý trong luồng tạo cửa sổ đích và chạy vòng lặp tin nhắn của nó, nó sẽ tạm dừng luồng, xử lý thông điệp trong ngữ cảnh của chuỗi VCL chính, gọi phương thức và tiếp tục chỉ sau khi phương thức đã thực hiện xong.

Vì vậy, có gì sai với nó (và điều gì tương tự sai khi sử dụng trực tiếp SendMessage())? Một số điều:

  • Buộc bất kỳ luồng nào để thực thi mã trong ngữ cảnh của một luồng khác buộc hai công tắc ngữ cảnh luồng, không cần phải đốt chu kỳ CPU.
  • Trong khi chuỗi VCL xử lý tin nhắn để gọi phương thức được đồng bộ hóa, nó không thể xử lý bất kỳ tin nhắn nào khác.
  • Khi nhiều chủ đề sử dụng phương pháp này, họ sẽ tất cả chặn và đợi Synchronize() hoặc SendMessage() để trả lại. Điều này tạo ra một nút cổ chai khổng lồ.
  • Có một bế tắc đang chờ xảy ra.Nếu chuỗi gọi Synchronize() hoặc SendMessage() trong khi đang giữ một đối tượng đồng bộ hóa và chuỗi VCL trong khi xử lý thư cần có cùng một đối tượng đồng bộ hóa, ứng dụng sẽ khóa. Cũng có thể nói về các cuộc gọi API chờ xử lý luồng - bằng cách sử dụng WaitForSingleObject() hoặc WaitForMultipleObjects() mà không có phương tiện xử lý tin nhắn sẽ gây ra bế tắc nếu chuỗi cần những cách này để "đồng bộ hóa" với chuỗi khác.

Vì vậy, những gì nên sử dụng? Một số tùy chọn, tôi sẽ mô tả một số:

  • Sử dụng PostMessage() thay vì SendMessage() (hoặc PostThreadMessage() nếu hai bài đều không phải là chủ đề VCL). Điều quan trọng là không sử dụng bất kỳ dữ liệu nào trong các thông số thư sẽ không còn hợp lệ khi thư đến, vì chuỗi gửi và nhận không được đồng bộ, vì vậy một số phương tiện khác phải được sử dụng để đảm bảo rằng bất kỳ chuỗi nào , tham chiếu đối tượng hoặc đoạn bộ nhớ vẫn hợp lệ khi thông báo được xử lý, mặc dù chuỗi gửi có thể không còn tồn tại nữa.

  • Tạo cấu trúc dữ liệu an toàn chủ đề, đặt dữ liệu cho chúng từ chuỗi công việc của bạn và tiêu thụ chúng từ chuỗi chính. Chỉ sử dụng PostMessage() để cảnh báo chuỗi VCL rằng dữ liệu mới đã được xử lý nhưng không đăng thông báo mỗi lần. Nếu bạn có một dòng dữ liệu liên tục, bạn thậm chí có thể có cuộc thăm dò chủ đề VCL cho dữ liệu (có thể bằng cách sử dụng bộ hẹn giờ), nhưng đây chỉ là phiên bản của người nghèo.

  • Không sử dụng các công cụ cấp thấp chút nào nữa. Nếu bạn ít nhất là trên Delphi 2007, hãy tải xuống OmniThreadLibrary và bắt đầu suy nghĩ về nhiệm vụ chứ không phải chủ đề. Thư viện này có rất nhiều cơ sở để trao đổi dữ liệu giữa các chủ đề và đồng bộ hóa. Nó cũng có thực hiện thread pool, là một điều tốt - bao nhiêu luồng bạn nên sử dụng không chỉ phụ thuộc vào ứng dụng mà còn trên phần cứng mà nó đang chạy, vì vậy nhiều quyết định có thể được thực hiện chỉ khi chạy. OTL sẽ cho phép bạn chạy các tác vụ trên một luồng thread thread, vì vậy hệ thống có thể điều chỉnh số lượng các luồng đồng thời khi chạy.

Edit:

On đọc lại tôi nhận ra rằng bạn không có ý định sử dụng SendMessage() nhưng PostMessage() - tốt, một số trên không áp dụng sau đó, nhưng tôi sẽ để nó tại chỗ. Tuy nhiên, có một số điểm hơn trong câu hỏi của bạn tôi muốn giải quyết:

Với lên đến 16 luồng chạy cùng một lúc (và hầu hết các xử lý các chủ đề con của mất từ ​​< 1 giây để ~ 10 giây) sẽ Messages Window là một thiết kế tốt hơn?

Nếu bạn đăng tin nhắn từ mỗi chuỗi một lần mỗi giây hoặc thậm chí lâu hơn, thì thiết kế sẽ ổn. Những gì bạn không nên làm là đăng hàng trăm hoặc nhiều tin nhắn trên mỗi luồng mỗi giây, bởi vì hàng đợi tin nhắn Windows có độ dài hữu hạn và tin nhắn tùy chỉnh không được can thiệp vào quá trình xử lý thông thường quá nhiều (chương trình của bạn sẽ bắt đầu xuất hiện không phản hồi).

nơi các bài viết chủ đề con một thông điệp cửa sổ (bao gồm một kỷ lục của một số chuỗi)

Một thông điệp cửa sổ có thể không chứa một kỷ lục.Nó mang hai tham số, một trong các loại WPARAM, loại khác là loại LPARAM. Bạn chỉ có thể truyền một con trỏ tới một bản ghi như vậy với một trong các loại này, vì vậy toàn bộ thời gian của bản ghi cần được quản lý bằng cách nào đó. Nếu bạn tự động phân bổ nó, bạn cũng cần phải giải phóng nó, dễ bị lỗi. Nếu bạn chuyển một con trỏ tới một bản ghi trên ngăn xếp hoặc đến một trường đối tượng, bạn cần đảm bảo rằng nó vẫn hợp lệ khi thông báo được xử lý, điều này sẽ khó hơn đối với các tin nhắn được gửi hơn là các tin nhắn đã gửi.

bạn có đề xuất bao gồm mã nơi tôi đăng lên lưới trong khối TCriticalSection (nhập và rời) không? Hoặc tôi sẽ không cần phải lo lắng về an toàn luồng vì tôi đang viết vào lưới trong chuỗi chính (mặc dù trong chức năng của trình xử lý tin nhắn cửa sổ)?

Không cần phải làm điều này, như cuộc gọi PostMessage() sẽ trở lại ngay lập tức, vì vậy không đồng bộ là cần thiết vào thời điểm này. Bạn chắc chắn sẽ cần phải lo lắng về an toàn luồng, rất tiếc bạn không thể biết khi. Bạn phải đảm bảo rằng quyền truy cập vào dữ liệu an toàn theo luồng, bằng cách luôn luôn khóa dữ liệu để truy cập, bằng cách sử dụng đối tượng đồng bộ hóa. Không thực sự là một cách để đạt được điều đó cho các bản ghi, dữ liệu luôn có thể được truy cập trực tiếp.

+0

"Tạo cấu trúc dữ liệu an toàn chủ đề" - đây là những gì tôi làm. Các chủ đề lưu trữ thông tin của họ ở đó và giao diện người dùng truy xuất thông tin đó trong bộ hẹn giờ hoặc tương tự. Điều này ngăn chặn bất kỳ chủ đề nào được giữ bởi giao diện người dùng. – mj2008

+0

Câu trả lời thú vị! Cảm ơn bạn đã giải thích chi tiết! – Mick

+10

Kể từ D6, TThread không sử dụng SendMessage nữa. Nó sử dụng một hàng đợi công việc an toàn thread nơi "công việc" dành cho chủ đề chính được đặt. Một thông báo được * đăng * vào chuỗi chính để cho biết rằng công việc có sẵn và chuỗi nền sẽ chặn trên một sự kiện. Khi vòng lặp tin nhắn chính sắp ngừng hoạt động, nó sẽ gọi "CheckSynchronize" để xem có công việc nào đang chờ hay không. Nếu vậy, nó xử lý nó. Khi một mục công việc được hoàn thành, sự kiện mà chủ đề nền bị chặn được thiết lập để cho biết hoàn thành. Được giới thiệu trong khung thời gian D2006, phương thức TThread.Queue đã được thêm vào không chặn. –

5

Mặc dù tôi chắc chắn có một cách phù hợp và một cách sai. Tôi đã viết mã bằng cách sử dụng cả hai phương pháp và một trong những tôi tiếp tục trở lại là phương pháp SendMessage và tôi không chắc chắn lý do tại sao.

Việc sử dụng SendMessage vs Synchronize thực sự không tạo ra bất kỳ sự khác biệt nào. Cả hai công trình về cơ bản giống nhau. Tôi nghĩ lý do tôi tiếp tục sử dụng SendMessage là tôi nhận thấy một lượng kiểm soát lớn hơn, nhưng tôi không biết.

Thường trình SendMessage làm cho chuỗi cuộc gọi dừng lại và chờ cho đến khi cửa sổ đích xử lý xong tin nhắn đã gửi. Do đó, chuỗi ứng dụng chính về cơ bản được đồng bộ hóa thành chuỗi con gọi cho thời lượng cuộc gọi. Bạn không cần sử dụng một phần quan trọng trong trình xử lý tin nhắn của cửa sổ.

Truyền dữ liệu về cơ bản là một chiều, từ chuỗi cuộc gọi đến chuỗi ứng dụng chính. Bạn có thể trả về một giá trị kiểu số nguyên trong message.result nhưng không có gì trỏ đến một đối tượng bộ nhớ trong luồng chính.

Vì hai luồng được "đồng bộ hóa" rằng điểm đó và chuỗi ứng dụng chính hiện được gắn với phản hồi SendMessage, bạn cũng không cần phải lo lắng về các chuỗi khác đang truy cập và xóa dữ liệu của mình cùng một lúc. Vì vậy, không bạn không cần phải lo lắng về việc sử dụng Phần quan trọng hoặc các loại biện pháp an toàn chủ đề khác.

Đối với những điều đơn giản, bạn có thể xác định một thư đơn (wm_threadmsg1) và sử dụng các trường wparam và lparam để chuyển các thông báo trạng thái (số nguyên) qua lại. Đối với các ví dụ phức tạp hơn, bạn có thể vượt qua một chuỗi bằng cách chuyển nó qua lparam và nhập nó trở lại một khoảng thời gian dài. A-la longint (pchar (myvar)) hoặc sử dụng pwidechar nếu bạn sử dụng D2009 hoặc mới hơn.

Nếu bạn đã làm việc với các phương pháp Đồng bộ hóa thì tôi sẽ không lo lắng về việc làm lại nó để thực hiện thay đổi.

+0

tôi cũng nghiêng về phía SendMessage . tôi cũng sử dụng WM_COPYDATA để chuyển nội bộ. – glob

9

BTW, bạn cũng có thể sử dụng TThread.Queue() thay vì TThread.Synchronize(). Queue() là phiên bản không đồng bộ, nó không chặn chuỗi cuộc gọi:

(Queue có sẵn từ D8).

tôi thích Synchronize() hoặc Queue(), bởi vì nó là dễ dàng hơn để hiểu (đối với lập trình viên khác) và OO tốt hơn so với thông điệp đơn giản gửi (không có kiểm soát về nó hoặc có thể gỡ lỗi nó!)

+1

FYI, Delphi 10.2 Tokyo đã thêm ['TThread.ForceQueue()'] (http://docwiki.embarcadero.com/Libraries/en/System.Classes.TThread.ForceQueue), tương tự như 'TThread.Queue()' ngoại trừ việc nó luôn luôn xếp hàng mã được chỉ định để thực hiện sau này * ngay cả khi được gọi trong luồng giao diện người dùng chính *. 'TThread.Queue()' không xếp hàng thực thi khi được gọi trong luồng giao diện người dùng chính, nó thực hiện ngay lập tức, như 'TThread.Synchronize()'. –

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