且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2022-05-26 09:19:09

线程安全"对您来说究竟意味着什么?别人呢?每次我看到这个问题,它最终都会归结为:我希望 VCL 是线程安全的,这样我就不必考虑线程和同步问题.我想编写我的代码,就好像它仍然是单线程."

What, precisely, does "thread-safe" mean to you? What about someone else? Every time I see this brought up, it ends up boiling down to this: "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 成为所谓的线程安全"方面做了多少工作,总会有遇到麻烦的情况.将如何使它成为线程安全的?我并不是说这是好斗的,我只是想证明这不是一个简单的适用于所有情况"的解决方案的简单问题.为了强调这一点,让我们看看一些潜在的解决方案".

No matter how much work went into making VCL so-called "thread-safe", there will always be situations where you can get into trouble. How would you go about making it thread-safe? I don't say this to be combative, rather I merely want to demonstrate that it is not a simple problem with a simple, "works-in-all-cases" solution. To highlight this, let's look at some potential "solutions."

我看到的最简单和最直接的方法是每个组件都有某种锁",比如互斥锁或临界区.组件上的每个方法都会在进入时获取锁,然后在退出前释放锁.让我们通过一个思想实验继续这条道路.考虑 Windows 如何处理消息:

The simplest and most direct approach I see is each component has some kind of "lock", say a mutex or critical section. Every method on the component grabs the lock on entry and then releases the lock just prior to exit. Let's continue down this path with a thought experiment. Consider how Windows processes messages:

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

Main thread obtains a message from the message queue and then dispatches it to the appropriate WndProc. This message is then routed to the appropriate TWinControl component. Since the component has a "lock", as the message is routed to the appropriate message handler on the component, the lock is acquired. So far so good.

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

Now take the proverbial button-click message processing. The OnClick message handler is now called which will most likely be a method on the owning TForm. Since the TForm descendant is also a TWinControl component, the TForm's lock is now acquired while the OnClick handler is processed. Now the button component is locked and the TForm component is also locked.

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

Continuing on this line of thinking, suppose the OnClick handler now wants add an item to a listbox, listview, or some other visual list or grid component. Now suppose some other thread (not the main UI thread) is already in the midst of accessing this same component. Once a method is called on the list from the UI thread it will attempt to acquire the lock, which it cannot since the other thread is currently holding it. As long as the non-UI thread doesn't hold that lock for very long, the UI thread will only block for a brief period.

到目前为止一切顺利,对吧?现在假设,当非 UI 线程持有列表控件的锁时,会调用一个通知事件.由于它很可能是拥有 TForm 的方法,因此在进入事件处理程序时,代码将尝试获取 TForm 的锁.

So far so good, right? Now suppose, that while the non-UI thread is holding the list control's lock, a notification event is called. Since, it will most likely be a method on the owning TForm, upon entry to the event handler, the code will attempt to acquire the lock for the TForm.

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

Do you see the problem? Remember the button OnClick handler? It already has the TForm lock in the UI thread! It is now blocked waiting for the lock on the list control, which the non-UI thread owns. This is a classic dead-lock. Thread A holds lock A and attempts to acquire lock B which is held by thread B. Thread B is at the same time attempting to acquire lock A.

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

Clearly, if every control/component has a lock that is automatically acquired and released for every method isn't a solution. What if we left the locking up to the user? Do you see how that also doesn't solve the problem either? How can you be certain that all the code you have (including any third-party components) properly locks/unlocks the controls/components? How does this keep the above scenario from happening?

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

What about a single shared lock for the whole of VCL? In this scenario, for each message that is processed, the lock is acquired while the message is processed regardless of what component the message is routed to. Again, how does this solve a similar scenario I described above? What if the user's code added other locks for synchronization with other non-UI threads? Even the simple act of blocking until a non-UI thread terminates can cause a dead lock if it is done while the UI thread holds the VCL lock.

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

What about non-UI components? Database, serial, network, containers, etc...? How should they be handled?

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

As excellently explained by the other answers, Windows already does a pretty decent job of properly segregating UI message processing to only the thread on which each HWND is created. In fact, learning precisely how Windows works in this regard will go a long way to understanding how you can write your code to work with Windows and VCL in a manner that avoids most of the pitfalls I highlighted above. The bottom line is writing multi-threaded code is difficult, requires a rather drastic mental shift, and lots of practice. Read as much as you can on multi-threading from as many sources as possible. Learn and understand as many coding examples of "thread-safe" code as you can, in any language.

希望这是有益的.