在主线程和子线程之间使用TThread的"同步"或使用窗口消息进行IPC更好吗?

Mic*_*ick 21 delphi multithreading ipc

我有一个用Delphi 2007编写的相当简单的多线程VCL gui应用程序.我在多个子线程(最多16个并发)中进行一些处理,需要更新主窗体上的网格控件(只需将字符串发布到网格中).没有任何子线程彼此交谈.

我的初始设计涉及调用TThread的"Synchronize"来更新当前运行的线程中的网格控件表单.但是,据我所知,调用Synchronize实际上就好像它是调用时的主线程一样.一次运行多达16个线程(并且大多数子线程的处理时间从<1秒到10秒),Window Messages是一个更好的设计吗?

我已经让它工作在这一点,子线程发布一个Windows消息(由几个字符串的记录组成),主线程有一个监听器,只是在收到消息时更新网格.

在这种情况下对IPC的最佳方法有何看法?窗口消息或'同步'?

如果我使用窗口消息,您是否建议将代码包装在我在TCriticalSection(进入和离开)块中发布到网格的位置?或者我不需要担心线程安全,因为我在主线程中写入网格(虽然在窗口消息处理程序的函数内)?

mgh*_*hie 36

编辑:

看起来自Delphi 4和5(Delphi版本我仍在使用的大部分工作中)后,许多实现细节都发生了变化,而Allen Bauer评论了以下内容:

从D6开始,TThread不再使用SendMessage了.它使用线程安全的工作队列,其中放置了用于主线程的"工作".将消息发布到主线程以指示工作可用且后台线程阻止事件.当主消息循环即将空闲时,它会调用"CheckSynchronize"以查看是否有任何工作正在等待.如果是这样,它会处理它.完成工作项后,将阻止后台线程的事件指示完成.在D2006时间帧中引入,添加了不阻止的TThread.Queue方法.

谢谢你的纠正.因此,请在原始答案中详细说明.

但这并不会真正影响核心要点.我仍然认为整个想法都Synchronize()存在致命的缺陷,而且当一个人试图保持现代机器的几个核心被占用时,这将是显而易见的.不要"同步"你的线程,让它们工作直到它们完成.尽量减少它们之间的所有依赖关系.特别是在更新GUI时,绝对没有理由等待它完成.无论是Synchronize()使用SendMessage()还是PostMessage(),产生的路障都是一样的.


你在这里所介绍的不是在所有的替代方案,如Synchronize()采用SendMessage()内部.所以这更像是一个问题,你想用什么武器射击自己的脚.

Synchronize()自从TThreadDelphi 2 VCL 推出以来,我们就一直与我们在一起,这真是一个耻辱,因为它是VCL中更大的设计错误之一.

它是如何工作的?它使用SendMessage()对主线程中创建的窗口的调用,并设置消息参数以传递要调用的无参数对象方法的地址.由于Windows消息将仅在创建目标窗口的线程中处理并运行其消息循环,因此将挂起线程,在主VCL线程的上下文中处理消息,调用方法,并仅在方法之后恢复线程已完成执行.

那么它有什么问题(以及SendMessage()直接使用的同样错误)?几件事:

  • 强制任何线程在另一个线程的上下文中执行代码会强制执行两个线程上下文切换,这会不必要地烧掉CPU周期.
  • 当VCL线程处理消息以调用synchronized方法时,它不能处理任何其他消息.
  • 当多个线程使用此方法时,它们将全部阻塞并等待Synchronize()SendMessage()返回.这造成了巨大的瓶颈.
  • 有一个等待发生的僵局.如果线程调用Synchronize()SendMessage()持有同步对象,并且处理消息时VCL线程需要获取相同的同步对象,则应用程序将锁定.
  • 等待线程句柄的API调用也是如此 - 如果线程需要这些方法与其他线程"同步",使用WaitForSingleObject()WaitForMultipleObjects()不使用某种方法处理消息将导致死锁.

那么使用什么呢?几个选项,我将描述一些:

  • 使用PostMessage()而不是SendMessage()(或者PostThreadMessage()如果两个线程都不是VCL线程).重要的是,不要在消息参数中使用任何数据,这些数据在消息到达时将不再有效,因为发送和接收线程根本不同步,因此必须使用其他一些方法来确保任何字符串处理消息时,对象引用或内存块仍然有效,即使发送线程可能不再存在.

  • 创建线程安全的数据结构,从工作线程将数据放入它们,并从主线程中使用它们.使用PostMessage()仅提醒VCL线程新的数据已经到达要处理,但每次不发帖.如果您有连续的数据流,您甚至可以对数据进行VCL线程轮询(可能使用计时器),但这只是一个穷人的版本.

  • 根本不要使用低级工具.如果您至少在Delphi 2007上,请下载OmniThreadLibrary并开始考虑任务,而不是线程.该库具有许多用于线程和同步之间的数据交换的工具.它还有一个线程池实现,这是一件好事 - 你应该使用多少线程不仅取决于应用程序,还取决于它运行的硬件,因此很多决定只能在运行时进行.OTL将允许您在线程池线程上运行任务,因此系统可以在运行时调整并发线程的数量.

编辑:

在重新阅读时,我意识到你不打算使用SendMessage()但是PostMessage()- 好吧,上面的一些不适用,但我会把它留在原地.但是,我想提出的问题还有一些要点:

一次运行多达16个线程(并且大多数子线程的处理时间从<1秒到10秒),Window Messages是一个更好的设计吗?

如果您每隔一秒甚至更长时间从每个帖子发布一条消息,那么设计就可以了.你不应该做的是每秒每个线程发布数百或更多的消息,因为Windows消息队列的长度有限,自定义消息不应该过多干扰正常的消息处理(你的程序会开始显示无响应).

子线程发布一条Windows消息(由几个字符串的记录组成)

窗口消息不能包含记录.它带有两个参数,一个是类型WPARAM,另一个是类型LPARAM.您只能将指向此类记录的指针强制转换为其中一种类型,因此需要以某种方式管理记录的生命周期.如果你动态分配它,你也需要释放它,这很容易出错.如果将指针传递给堆栈上的记录或对象字段,则需要确保在处理消息时它仍然有效,这对于发布的消息比发送的消息更难.

你建议在我在TCriticalSection(进入和离开)块中发布到网格的代码吗?或者我不需要担心线程安全,因为我在主线程中写入网格(虽然在窗口消息处理程序的函数内)?

有没有必要做这个,因为PostMessage()调用将立即返回,所以不需要同步在这一点上.你肯定需要担心线程安全,遗憾的是你无法知道什么时候.您必须始终使用同步对象锁定要访问的数据,以确保对数据的访问是线程安全的.实际上没有办法实现记录,数据总是可以直接访问.

  • 从D6开始,TThread不再使用SendMessage了.它使用线程安全的工作队列,其中放置了用于主线程的"工作".消息被*发布*到主线程以指示工作可用且后台线程阻塞事件.当主消息循环即将空闲时,它会调用"CheckSynchronize"以查看是否有任何工作正在等待.如果是这样,它会处理它.完成工作项后,将阻止后台线程的事件指示完成.在D2006时间帧中引入,添加了不阻止的TThread.Queue方法. (10认同)

And*_*dré 9

顺便说一句,你也可以用TThread.Queue()而不是TThread.Synchronize(). Queue()是异步版本,它不会阻止调用线程:

(Queue自D8起可用).

我更喜欢Synchronize()或者Queue(),因为它更容易理解(对于其他程序员)和更好的OO而不是简单的消息发送(没有控制它或能够调试它!)

  • 仅供参考,Delphi 10.2东京已添加[`TThread.ForceQueue()`](http://docwiki.embarcadero.com/Libraries/en/System.Classes.TThread.ForceQueue),类似于`TThread.Queue() `除非它总是将指定的代码排队以便以后执行*,即使在主UI线程中调用*也是如此.`TThread.Queue()`在主UI线程中调用时不会排队执行,它会立即执行,就像`TThread.Synchronize()`一样. (2认同)

Rya*_*lls 5

虽然我确信有正确的方法和错误的方式.我用两种方法编写代码,而我一直在返回的是SendMessage方法,我不知道为什么.

SendMessage vs Synchronize的使用并没有什么区别.两者的工作基本相同.我认为我继续使用SendMessage的原因是我感觉到更多的控制,但我不知道.

SendMessage例程使调用线程暂停并等待,直到目标窗口完成处理发送的消息.因此,主要应用程序线程在调用期间基本上与调用子线程同步.您不需要在Windows消息处理程序中使用临界区.

从调用线程到主应用程序线程,数据传输基本上是一种方式.您可以在message.result中返回整数类型值,但不指向主线程中的内存对象.

由于这两个线程是"同步"的,因此该点与主应用程序线程当前绑定响应SendMessage,那么您也不需要担心其他线程进入并同时丢弃您的数据.因此,您不必担心使用关键部分或其他类型的线程安全措施.

对于简单的事情,您可以定义单个消息(wm_threadmsg1)并使用wparam和lparam字段来回传输(整数)状态消息.对于更复杂的示例,您可以通过将其传递给lparam并将其转换回longint来传递字符串.A-la longint(pchar(myvar))或使用pwidechar如果您使用D2009或更新版本.

如果您已经使用了Synchronize方法,那么我不会担心重新进行更改.