传输事件处理程序中的 SynchronizationContext.Post(...)
我们有一个方法,由于客户端应用程序中的线程需要使用 SynchronizationContext。
我的一位同事编写的一些代码对我来说“感觉”不正确,性能分析器告诉我,在这段代码中使用了很多处理。
void transportHelper_SubscriptionMessageReceived(object sender, SubscriptionMessageEventArgs e)
{
if (SynchronizationContext.Current != synchronizationContext)
{
synchronizationContext.Post(delegate
{
transportHelper_SubscriptionMessageReceived(sender, e);
}, null);
return;
}
[code removed....]
}
这对我来说感觉不太对劲,因为我们基本上将相同的请求发布到 gui 线程事件队列...但是,除了该代码区域的性能之外,我也看不到任何明显的问题。
此方法是一个事件处理程序,附加到由我们的中间层消息传递层帮助程序 (transportHelper) 引发的事件,并且它存在于处理来自 GUI 的请求的服务中。
这看起来是一种可以接受的确保我们不会出现跨线程错误的方法吗? 如果没有,有更好的解决方案吗?
谢谢
We have a method which, due to threading in the client application requires the usage of SynchronizationContext.
There is a bit of code which one of my colleagues has written which doesnt "feel" right to me, and a performance profiler is telling me that quit a lot of processing is being used in this bit of code.
void transportHelper_SubscriptionMessageReceived(object sender, SubscriptionMessageEventArgs e)
{
if (SynchronizationContext.Current != synchronizationContext)
{
synchronizationContext.Post(delegate
{
transportHelper_SubscriptionMessageReceived(sender, e);
}, null);
return;
}
[code removed....]
}
This just doesnt feel right to me, as we are basically posting the same request to the gui thread event queue...however, I cannot see anyhting oviously problematic either, other than the performance of this area of code.
This method is an event handler attached to an event raised by our middle-tier messaging layer helper (transportHelper) and it exists within a service which handles requests from the GUI.
Does this seem like an acceptable way of making sure that we do not get cross-thread errors? If not, is there a better solution?
Thanks
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
让我们跟踪这个方法内部发生了什么,看看它告诉我们什么。
方法签名遵循事件处理程序的签名,正如问题所示,我们可以预期它会在非 UI 线程的某个线程的上下文中首先调用。
该方法所做的第一件事是将其运行的线程的 SynchronizationContext 与保存在成员变量中的 SynchronizationContext 进行比较。 我们假设保存的上下文是 UI 线程的上下文。 (Mike Peretz 在 CodeProject 上发布了一系列有关 SynchronizationContext 类的精彩介绍文章)
该方法会发现上下文不相等,因为它是在与 UI 线程不同的线程中调用的。 调用线程的上下文可能为 null,其中 UI 线程的上下文几乎可以保证设置为 WindowsFormsSynchronizationContext 的实例。 然后,它将在 UI 上下文上发出 Post(),将委托传递给自身及其参数,并立即返回。 这完成了后台线程上的所有处理。
Post() 调用会导致在 UI 线程上调用完全相同的方法。 跟踪 WindowsFormsSynchronizationContext.Post() 的实现表明,这是通过在 UI 线程的消息队列上排队自定义 Windows 消息来实现的。 参数会被传递,但不会被“封送”,因为它们不会被复制或转换。
由于 Post() 调用的结果,我们的事件处理程序方法现在被再次调用,并且参数完全相同。 然而,这一次,线程的 SynchronizationContext 和保存的上下文是同一个。 if 子句的内容被跳过,并执行 [代码已删除] 部分。
这是一个好的设计吗? 如果不知道[代码已删除]部分的内容,很难说。 以下是一些想法:
从表面上看,这似乎并不是一个糟糕的设计。 消息在后台线程上接收,并传递到 UI 线程进行呈现。 调用者立即返回去做其他事情,而接收者则继续执行任务。 这有点类似于 Unix fork() 模式。
该方法是递归的,以独特的方式。 它不会在同一线程上调用自身。 相反,它会导致不同的线程调用它。 与任何递归代码一样,我们会关心它的终止条件。 通过阅读代码,可以相当安全地假设,当传递给 UI 线程时,它始终会被递归调用一次。 但这是另一个需要注意的问题。 另一种设计可能会向 Post() 传递不同的方法(也许是匿名方法),并完全避免递归问题。
if 子句中发生大量处理似乎没有明显的原因。 使用 .NET 反射器 查看 Post() 的 WindowsFormsSynchronizationContext 实现,揭示了一些中等长度的序列其中有很多代码,但没有什么太奇特的; 这一切都发生在 RAM 中,并且不会复制大量数据。 本质上,它只是准备参数并将 Windows 消息放入接收线程的消息队列中。
对于
您应该检查该方法的[代码已删除]部分内发生的情况。 涉及 UI 控件的代码完全属于那里——它必须在 UI 线程内执行。 但是,如果其中有不处理 UI 的代码,那么让它在接收线程中执行可能是一个更好的主意。 例如,任何 CPU 密集型解析最好托管在接收线程中,这样不会影响 UI 响应能力。 您可以将这部分代码移至 if 子句上方,然后将其余代码移至单独的方法中,以确保这两个部分都不会执行两次。
如果接收线程和 UI 线程都需要保持响应,例如进一步传入消息和用户输入,您可能需要引入第三个线程来处理消息,然后再将消息传递到 UI 线程。
Let's trace what's going on inside this method, and see what that tells us.
The method signature follows that of event handlers, and as the question indicates, we can expect it to be first called in the context of some thread that is not the UI thread.
The first thing the method does is to compare the SynchronizationContext of the thread it's running in with a SynchronizationContext saved in a member variable. We'll assume the saved context is that of the UI thread. (Mike Peretz posted an excellent series of introductory articles to the SynchronizationContext class on CodeProject)
The method will find the contexts not equal, as it is called in a thread different from the UI thread. The calling thread's context is likely to be null, where the UI thread's context is pretty much guarantied to be set to an instance of WindowsFormsSynchronizationContext. It will then issue a Post() on the UI context, passing a delegate to itself and its arguments, and return immediately. This finishes all processing on the background thread.
The Post() call causes the exact same method to be invoked on the UI thread. Tracing the implementation of WindowsFormsSynchronizationContext.Post() reveals that this is implemented by queueing a custom Windows message on the UI thread's message queue. Arguments are passed, but are not "marshaled", in the sense that they aren't copied or converted.
Our event handler method is now called again, as a result of the Post() call, with the exact same arguments. This time around, however, the thread's SynchronizationContext and the saved context are one and the same. The content of the if clause is skipped, and the [code removed] portion is executed.
Is this a good design? It's hard to say without knowing the content of the [code removed] portion. Here are some thoughts:
Superficially, this doesn't seem to be a horrible design. A message is received on a background thread, and is passed on to the UI thread for presentation. The caller returns immediately to do other things, and the receiver gets to continue with the task. This is somewhat similar to the Unix fork() pattern.
The method is recursive, in a unique way. It doesn't call itself on the same thread. Rather, it causes a different thread to invoke it. As with any recursive piece of code, we would be concerned with its termination condition. From reading the code, it appears reasonably safe to assume that it will always be invoked recursively exactly once, when passed to the UI thread. But it's another issue to be aware of. An alternative design might have passed a different method to Post(), perhaps an anonymous one, and avoid the recursion concern altogether.
There doesn't seem to be an obvious reason for a large amount of processing to occur inside the if clause. Reviewing the WindowsFormsSynchronizationContext implementation of Post() with the .NET reflector reveals some moderately long sequences of code in it, but nothing too fancy; It all happens in RAM, and it does not copy large amounts of data. Essentially it just prepares the arguments and queues a Windows message on the receiving thread's message queue.
You should review what is going on inside the [code removed] portion of the method. Code that touches UI controls totally belongs there -- it must execute inside the UI thread. However, if there is code in there that doesn't deal with UI, it might be a better idea to have it execute in the receiving thread. For example, any CPU-intensive parsing would be better hosted in the receiving thread, where it does not impact the UI responsiveness. You could just move that portion of the code above the if clause, and move the remaining code to a separate method -- to ensure neither portion gets executed twice.
If both the receiving thread and the UI thread need to remain responsive, e.g. both to further incoming message and to user input, you might need to introduce a third thread to process the messages before passing them to the UI thread.