Delphi:为什么VCL不是线程安全的?怎么会这样?

时间:2015-02-22 07:57:51

标签: multithreading delphi thread-safety vcl

到处都注意到VCL不是线程安全的,我们必须同步访问它。所以它的VCL故障不是线程安全的。

VCL本身如何是线程安全的?

3 个答案:

答案 0 :(得分:14)

“线程安全”对您来说意味着什么?别人怎么样?每次我看到这一点,它最终都会沸腾到这样:“我希望VCL是线程安全的,所以我不必考虑线程和同步问题。我想把我的代码写成好像它仍然是单线程“。

无论在制作VCL所谓的“线程安全”方面做了多少工作,总会有一些情况会让你陷入麻烦。 将如何使其成为线程安全的?我不是说这是好斗的,而是我只是想证明一个简单的“全包工作”解决方案不是一个简单的问题。为了突出这一点,让我们看看一些潜在的“解决方案”。

我看到的最简单和最直接的方法是每个组件都有某种“锁定”,比如互斥或临界区。组件上的每个方法都会在输入时获取锁定,然后在退出之前释放锁定。让我们继续沿着这条路走thought experiment。考虑Windows如何处理消息:

主线程从消息队列中获取消息,然后将其分派给相应的WndProc。然后,此消息将路由到相应的TWinControl组件。由于组件具有“锁定”,因此当消息被路由到组件上的适当消息处理程序时,将获取锁定。到目前为止一切都很好。

现在进行众所周知的按钮点击消息处理。现在调用OnClick消息处理程序,它很可能是拥有TForm的方法。由于TForm后代也是TWinControl组件,因此现在可以在处理OnClick处理程序时获取TForm的锁。现在按钮组件被锁定,TForm组件也被锁定。

继续这一思路,假设OnClick处理程序现在想要将项添加到列表框,列表视图或其他一些可视列表或网格组件。现在假设一些其他线程(不是主UI线程)已经在访问这个相同的组件。一旦从UI线程在列表上调用了一个方法,它将尝试获取锁,但由于另一个线程当前正在持有它,因此它无法获取锁。只要非UI线程长时间不持有该锁定,UI线程将仅在短时间内阻塞。

到目前为止这么好,对吗?现在假设,当非UI线程持有列表控件的锁时,会调用通知事件。因为,它很可能是拥有TForm的方法,在进入事件处理程序时,代码将尝试获取TForm的锁。

你看到了问题吗?还记得按钮OnClick处理程序吗? 已经在UI线程中有TForm锁!它现在被阻塞,等待非UI线程拥有的列表控件上的锁定。 这是一个经典的死锁。线程A持有锁A并尝试获取由线程B持有的锁B.线程B同时尝试获取锁A。

显然,如果每个控件/组件都有一个自动获取并为每个方法释放的锁定不是解决方案。如果我们将锁定保留给用户怎么办?您是否也看到这也无法解决问题?您如何确定您拥有的所有代码(包括任何第三方组件)是否正确锁定/解锁控​​件/组件?这是如何阻止上述情况发生的?

整个VCL的单个共享锁怎么样?在此方案中,对于处理的每条消息,无论消息路由到哪个组件,都会在处理消息时获取锁定。再次,这如何解决我上面描述的类似场景?如果用户的代码添加了其他锁以与其他非UI线程同步,该怎么办?即使是非UI线程终止之前的简单阻塞行为,如果在UI线程持有VCL锁定时完成,也会导致死锁。

非UI组件怎么样?数据库,串口,网络,容器等......?应如何处理?

正如其他答案所解释的那样,Windows已经做了相当不错的工作,正确地将UI消息处理分离到仅创建每个HWND的线程。事实上,准确了解Windows如何在这方面工作将有助于理解如何编写代码以使用Windows和VCL,以避免上面提到的大多数陷阱。底线是编写多线程代码很困难,需要相当剧烈的心理转变,以及大量的练习。尽可能多地从多个线程读取尽可能多的线程。用任何语言学习和理解尽可能多的“线程安全”代码编码示例。

希望这是有益的。

答案 1 :(得分:7)

VCL不是线程安全的。它是Win32的包装器。 Win32是线程安全的,但具有为该语句赋予意义的线程规则。最具体地说,窗口与创建它的线程具有亲缘关系。

Windows消息队列的设计意味着几乎总是希望主线程创建所有GUI窗口。 VCL设计师认为只支持这种操作模式是合理的。因此必须从主线程执行所有VCL代码。

没有什么可以改变这一点。这是设计的。如果您希望执行VCL代码,则必须在主线程上执行。使用TThread.SynchronizeTThread.Queue进行排列。

答案 2 :(得分:7)

VCL(特别是UI控件)不是线程安全的原因有很多。

  1. 消息输入的竞争条件,特别是在直接调用TControl.Perform() / TObject.Dispatch()但不使用PostMessage() / SendMessage()的代码中。前者不执行控件的消息处理程序的任何同步,但后者确实如此。因此,从主线程外部执行基于非HWND的消息是不安全的。

  2. HWND具有线程关联性。它只接收和处理消息,并且只能在创建它的线程上下文中销毁。 TWinControl可以在其生命周期内随时甚至多次销毁并重建其HWND。如果不存在,TWinControl.Handle属性getter将创建一个新的HWND。因此,如果控件在另一个线程从Handle属性读取时正在重新创建其HWND,则控件最终可能会在错误的线程上下文中创建一个新的HWND,从而使控件不再响应主消息循环(也可能泄漏第二个HWND)。因此,从主线程外部读取TWinControl.Handle属性是不安全的。

  3. VCL有一个MakeObjectInstance()函数,它创建一个动态代理,允许TWndMethod类方法用作Win32 WNDPROC窗口回调过程。所有TWinControl控件和一些实用程序类(如TTimer)都使用此函数。在内部,它维护一个全局链接的代理列表,并且该列表不受跨线程的并发访问的保护。因此,从主线程外部创建/销毁基于HWND的VCL控件是不安全的。

  4. 我确定还有其他原因,但这些都很重要。