C# 需要第二(和第三)个意见来解决这个Winforms竞态问题吗
在博客等中,有一百个例子介绍了如何实现后台工作程序来记录前台GUI元素或为其提供状态。其中大多数都包含一种方法来处理在生成工作线程和使用ShowDialog()创建前台对话框之间存在的争用条件。然而,我突然想到,一种简单的方法是强制在表单构造函数中创建句柄,这样线程就不能在创建句柄之前触发对表单的Invoke/BeginInvoke调用 考虑一个使用后台工作线程登录到前台的Logger类的简单示例 另外,假设我们不希望NLog或其他重型框架做这么简单和轻量级的事情 前台线程使用ShowDialog()打开我的日志程序窗口,但仅在后台“worker”线程启动后打开。工作线程调用logger.Log(),它本身使用logForm.BeginInvoke()在前台线程上正确更新日志控件C# 需要第二(和第三)个意见来解决这个Winforms竞态问题吗,c#,.net,winforms,multithreading,.net-3.5,C#,.net,Winforms,Multithreading,.net 3.5,在博客等中,有一百个例子介绍了如何实现后台工作程序来记录前台GUI元素或为其提供状态。其中大多数都包含一种方法来处理在生成工作线程和使用ShowDialog()创建前台对话框之间存在的争用条件。然而,我突然想到,一种简单的方法是强制在表单构造函数中创建句柄,这样线程就不能在创建句柄之前触发对表单的Invoke/BeginInvoke调用 考虑一个使用后台工作线程登录到前台的Logger类的简单示例 另外,假设我们不希望NLog或其他重型框架做这么简单和轻量级的事情 前台线程使用ShowDialo
public override void Log(string s)
{
form.BeginInvoke(logDelegate, s);
}
其中logDelegate只是“form.Log()”或其他一些可能更新进度条的代码的简单包装器
问题在于存在的竞争条件;当后台工作线程在调用前台ShowDialog()之前开始记录时,表单的句柄尚未创建,因此BeginInvoke()调用失败
我熟悉各种方法,包括使用表单OnLoad事件和计时器创建挂起的工作任务,直到OnLoad事件生成一条计时器消息,在显示表单后启动任务,或者如前所述,使用消息队列。但是,我认为只要强制对话框的句柄尽早创建(在构造函数中),就可以确保没有竞争条件,假设线程是由创建对话框的同一线程派生的
MSDN说:“如果句柄尚未创建,引用此属性将强制创建句柄。”
因此,我的记录器包装了一个表单,其构造函数执行以下操作:
public SimpleProgressDialog() {
var h = form.Handle; // dereference the handle
}
这个解决方案似乎太简单而不正确。我特别感兴趣的是,为什么这个看似过于简单的解决方案使用起来是安全的还是不安全的
有什么评论吗?我还缺什么吗
编辑:我不是在要求其他选择。不询问如何使用NLog或Log4net等。如果我是,我会写一页关于此应用程序上所有客户限制的内容,等等
根据投票数,还有很多其他人也想知道答案。我的二分钱:如果日志框架只是在句柄尚未创建时维护一个未显示日志条目的缓冲区,那么就没有必要强制提前创建句柄。它可以实现为一个
队列
,或者其他许多东西。在.NET中搞乱句柄创建的顺序让我感到恶心
我认为唯一的危险是性能下降。winforms中的句柄创建被延迟以加快速度。但是,由于这听起来像是一个一次性操作,因此成本不高,因此我认为您的方法很好。因为您确实在调用线程上创建了窗口,所以可能会导致死锁。如果创建窗口的线程没有运行消息泵,则BeginInvoke会将您的委托调用添加到消息队列中,该队列将永远不会被清空,如果您在处理窗口消息的同一线程上没有Application.Run() 为每个日志消息发送窗口消息也非常慢。最好有一个生产者-消费者模型,在该模型中,您的日志线程将消息添加到另一个线程清空的队列中。唯一需要锁定的时间是在消息排队或退队时。当发出事件信号或超时(例如100ms)已过时,使用者线程可以等待具有超时的事件开始处理下一条消息
可以找到线程安全的阻塞队列 如果您担心引用
Control.Handle
依赖于副作用来创建句柄,您只需调用Control.CreateControl()
即可创建它。但是,如果属性已经存在,则引用该属性的好处是不初始化它
至于这是否安全,假设句柄已创建,您是正确的:只要您在同一线程上生成后台任务之前创建句柄,就可以避免争用条件。您可以始终检查表单的
IsHandleCreated
属性,查看句柄是否已生成;然而,有一些警告。我也遇到过类似的情况,winforms控件会随着大量多线程的进行而动态创建/销毁。我们最终使用的模式有点像这样:
private void SomeEventHandler(object sender, EventArgs e) // called from a bg thread
{
MethodInvoker ivk = delegate
{
if(this.IsDisposed)
return; // bail out! Run away!
// maybe look for queued stuff if it exists?
// the code to run on the UI thread
};
if(this.IsDisposed)
return; // run away! killer rabbits with pointy teeth!
if(!this.IsHandleCreated) // handle not built yet, do something in the meantime
DoSomethingToQueueTheCall(ivk);
else
this.BeginInvoke(ivk);
}
这里的一个重要教训是,如果您试图在处理表单后与表单进行交互,则会出现一个kaboom。不要依赖于invokererequired
,因为如果控件的句柄尚未创建,它将在任何线程上返回false。也不要仅仅依赖于IsHandleCreated
,因为在处理控件后,它将返回false
基本上,您有三个标志,它们的状态将告诉您需要了解的有关控件初始化状态的信息,以及您是否处于相对于控件的BG线程上
控件可以处于以下三种初始化状态之一:
- 未初始化,尚未创建句柄
在每个线程上返回falseinvokererequired
返回falseIsHandleCreated
返回falseIsDisposed
- 已初始化、就绪、活动
按照文档中的说明执行invokererequired
返回trueIsHandleCreated
返回false<IsDisposed