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

Delphi: Why VCL is not thread-safe? How can be?

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

VCL 本身如何做到线程安全?

VCL 不是线程安全的。它是 Win32 的包装器。 Win32 是线程安全的,但具有赋予该语句意义的线程规则。最具体地说,window 与创建它的线程具有关联性。

Windows 消息队列的设计意味着让主线程创建所有 GUI windows 几乎总是更可取。 VCL 设计者认为只支持该操作模式是合理的。因此所有 VCL 代码都必须从主线程执行。

没有任何办法可以改变这一点。这是设计使然。如果你想执行 VCL 代码,它必须在主线程上完成。使用 TThread.SynchronizeTThread.Queue 来安排。

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

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

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

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

我敢肯定还有其他原因,但这些是最重要的。

确切地说,"thread-safe" 对您意味着什么?别人呢?每次我看到这个问题时,它最终都会归结为:"I want VCL to be thread-safe so I don't have to think about threading and synchronization issues. I want to write my code as if it is still single-threaded."

无论为了使 VCL 成为所谓的 "thread-safe" 付出了多少努力,总会有遇到麻烦的情况。 you 如何让它成为线程安全的?我不是说这是好斗的,而是我只是想证明这不是一个简单的问题,有一个简单的 "works-in-all-cases" 解决方案。为了强调这一点,让我们看看一些潜在的 "solutions."

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

主线程从消息队列中获取一条消息,然后将其分派给合适的WndProc。此消息然后被路由到适当的 TWinControl 组件。由于组件有一个 "lock",当消息被路由到组件上适当的消息处理程序时,将获取锁。到目前为止一切顺利。

现在进行众所周知的按钮单击消息处理。现在调用 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。

显然,如果每个 control/component 都有一个针对每个方法自动获取和释放的锁,则不是解决方案。如果我们将锁定留给用户怎么办?您是否看到这也不能解决问题?您如何确定您拥有的所有代码(包括任何第三方组件)正确 locks/unlocks controls/components?这如何防止上述情况发生?

整个 VCL 的单个共享锁怎么样?在这种情况下,对于处理的每条消息,都会在处理消息时获取锁,而不管消息路由到哪个组件。同样,这如何解决我上面描述的类似情况?如果用户的代码添加了其他锁以与其他非UI 线程同步怎么办?如果在 UI 线程持有 VCL 锁时完成,即使是简单的阻塞直到非 UI 线程终止也会导致死锁。

非UI 组件呢?数据库、串行、网络、容器等...?应该如何处理?

正如其他答案所很好地解释的那样,Windows 已经做了相当不错的工作,将 UI 消息处理正确地隔离到仅创建每个 HWND 的线程。事实上,准确了解 Windows 在这方面的工作原理将大大有助于理解如何编写代码以使用 Windows 和 VCL,从而避免我上面强调的大部分陷阱.底线是编写多线程代码是困难的,需要相当剧烈的思维转变和大量练习。从尽可能多的资源中尽可能多地阅读多线程。尽可能多地学习和理解 "thread-safe" 代码的编码示例,使用任何语言。

希望这对您有所帮助。