C# 为什么在UI线程上输入锁会触发OnPaint事件?
我遇到了一些我根本不明白的事情。 在我的应用程序中,我有几个线程,所有线程都向共享集合添加(和删除)项(使用共享锁)。 UI线程使用计时器,并且在每次勾选时都使用集合更新其UI 由于我们不希望UI线程长时间持有锁并阻止其他线程,因此我们的做法是,首先获取锁,复制集合,释放锁,然后处理副本。 代码如下所示:C# 为什么在UI线程上输入锁会触发OnPaint事件?,c#,winforms,multithreading,C#,Winforms,Multithreading,我遇到了一些我根本不明白的事情。 在我的应用程序中,我有几个线程,所有线程都向共享集合添加(和删除)项(使用共享锁)。 UI线程使用计时器,并且在每次勾选时都使用集合更新其UI 由于我们不希望UI线程长时间持有锁并阻止其他线程,因此我们的做法是,首先获取锁,复制集合,释放锁,然后处理副本。 代码如下所示: public void GUIRefresh() { ///... List<Item> tmpList; lock (Locker) {
public void GUIRefresh()
{
///...
List<Item> tmpList;
lock (Locker)
{
tmpList = SharedList.ToList();
}
// Update the datagrid using the tmp list.
}
请注意,输入锁(Monitor.Enter)之后是NativeWindow.Callback,这将导致OnPaint
- 这怎么可能?UI线程是否被劫持以检查其消息泵?这有意义吗?或者这里还有别的东西吗
- 有办法避免吗?我不想从锁内调用OnPaint
Joe Duffy的书《Windows上的并发编程》也介绍了这一点。GUI应用程序的主线程是一个STA线程、单线程单元。注意程序Main()方法上的[StatThread]属性。STA是一个COM术语,它为基本上线程不安全的组件提供了一个好客的家,允许从工作线程调用它们。COM在.NET应用程序中仍然非常活跃。拖放、剪贴板、外壳对话框(如OpenFileDialog)和常见控件(如WebBrowser)都是单线程COM对象。STA是UI线程的硬需求 STA线程的行为约定是,它必须泵送消息循环,并且不允许阻塞。阻塞很可能导致死锁,因为它不允许对这些单元线程COM组件进行封送处理。您正在使用lock语句阻塞线程 CLR非常了解这一要求,并为此做了一些事情。阻止诸如Monitor.Enter()、WaitHandle.WaitOne/Any()或Thread.Join()之类的调用以泵送消息循环。实现这一点的本机Windows API是消息循环分派Windows消息以使STA保持活动状态,包括绘制消息。这会导致重入问题当然,油漆不应该是一个问题 这里面有很好的背景资料 也许这一切都敲响了警钟,你可能会情不自禁地注意到这听起来很像一个调用Application.DoEvents()的应用程序。可能是解决UI冻结问题最可怕的方法。这是一个相当准确的心理模型,它描述了引擎盖下发生的事情,DoEvents()也会推动消息循环。唯一的区别是,CLR的等价物在允许发送哪些消息方面更具选择性,它会过滤这些消息。与DoEvents()不同,DoEvents()发送所有内容。不幸的是,无论是Brumme的帖子还是SSCLI20源代码都没有足够的详细信息来准确地知道要调度什么,实际的CLR函数在源代码中不可用,而且太大,无法反编译。但很明显,你可以看到它不会过滤WM_油漆。它将过滤真正的麻烦制造者,输入事件通知,比如允许用户关闭窗口或单击按钮的通知
功能,而不是bug。通过移除阻塞和依赖封送回调,避免重新进入的麻烦。BackgroundWorker.RunWorkerCompleted是一个典型的例子。我在等待句柄阻塞时发现了这个问题。对此的回答给了我下一步实施的提示:
public static class NativeMethods
{
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern UInt32 WaitForSingleObject(SafeWaitHandle hHandle, UInt32 dwMilliseconds);
}
public static class WaitHandleExtensions
{
const UInt32 INFINITE = 0xFFFFFFFF;
const UInt32 WAIT_ABANDONED = 0x00000080;
const UInt32 WAIT_OBJECT_0 = 0x00000000;
const UInt32 WAIT_TIMEOUT = 0x00000102;
const UInt32 WAIT_FAILED = INFINITE;
/// <summary>
/// Waits preventing an I/O completion routine or an APC for execution by the waiting thread (unlike default `alertable` .NET wait). E.g. prevents STA message pump in background.
/// </summary>
/// <returns></returns>
/// <seealso cref="http://stackoverflow.com/questions/8431221/why-did-entering-a-lock-on-a-ui-thread-trigger-an-onpaint-event">
/// Why did entering a lock on a UI thread trigger an OnPaint event?
/// </seealso>
public static bool WaitOneNonAlertable(this WaitHandle current, int millisecondsTimeout)
{
if (millisecondsTimeout < -1)
throw new ArgumentOutOfRangeException("millisecondsTimeout", millisecondsTimeout, "Bad wait timeout");
uint ret = NativeMethods.WaitForSingleObject(current.SafeWaitHandle, (UInt32)millisecondsTimeout);
switch (ret)
{
case WAIT_OBJECT_0:
return true;
case WAIT_TIMEOUT:
return false;
case WAIT_ABANDONED:
throw new AbandonedMutexException();
case WAIT_FAILED:
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
default:
return false;
}
}
}
公共静态类NativeMethods
{
[DllImport(“kernel32.dll”,SetLastError=true)]
内部静态外部UInt32 WaitForSingleObject(SafeWaitHandle hHandle,UInt32 dw毫秒);
}
公共静态类WaitHandleExtensions
{
常数UInt32无穷=0xFFFFFFFF;
const UInt32 WAIT_放弃=0x00000080;
const UInt32 WAIT_OBJECT_0=0x00000000;
const UInt32 WAIT_TIMEOUT=0x00000102;
const UInt32 WAIT_FAILED=无限;
///
///等待阻止I/O完成例程或APC由等待线程执行(与默认的'alertable`.NET wait不同)。例如,在后台阻止STA消息泵。
///
///
///
///为什么在UI线程上输入锁会触发OnPaint事件?
///
公共静态bool WaitOneNonAlertable(此WaitHandle当前值,int毫秒)
{
如果(毫秒刺激<-1)
抛出新的ArgumentOutOfRangeException(“毫秒计时”,毫秒计时,“错误等待超时”);
uint-ret=NativeMethods.WaitForSingleObject(current.SafeWaitHandle,(UInt32)毫秒计时);
开关(ret)
{
案例等待对象0:
返回true;
案例等待超时:
返回false;
案件被放弃:
抛出新的废弃MutexException();
案例等待失败:
抛出新的System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
违约:
返回false;
}
}
}
如何捕获堆栈跟踪?我们持有对线程的引用。然后我们执行thread.Suspend();日志(新的StackTrace(thread,true).ToString());thread.Resume();在VisualStudio中调试应用程序时,我得到了类似的stacktrace。当DataGridView开始非常缓慢地绘制时,我刚刚点击了暂停(这样你就可以看到每个单元格突然被绘制),并注意到它正在从一个锁处理OnPaint
(我的stacktrace显示“ma
public static class NativeMethods
{
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern UInt32 WaitForSingleObject(SafeWaitHandle hHandle, UInt32 dwMilliseconds);
}
public static class WaitHandleExtensions
{
const UInt32 INFINITE = 0xFFFFFFFF;
const UInt32 WAIT_ABANDONED = 0x00000080;
const UInt32 WAIT_OBJECT_0 = 0x00000000;
const UInt32 WAIT_TIMEOUT = 0x00000102;
const UInt32 WAIT_FAILED = INFINITE;
/// <summary>
/// Waits preventing an I/O completion routine or an APC for execution by the waiting thread (unlike default `alertable` .NET wait). E.g. prevents STA message pump in background.
/// </summary>
/// <returns></returns>
/// <seealso cref="http://stackoverflow.com/questions/8431221/why-did-entering-a-lock-on-a-ui-thread-trigger-an-onpaint-event">
/// Why did entering a lock on a UI thread trigger an OnPaint event?
/// </seealso>
public static bool WaitOneNonAlertable(this WaitHandle current, int millisecondsTimeout)
{
if (millisecondsTimeout < -1)
throw new ArgumentOutOfRangeException("millisecondsTimeout", millisecondsTimeout, "Bad wait timeout");
uint ret = NativeMethods.WaitForSingleObject(current.SafeWaitHandle, (UInt32)millisecondsTimeout);
switch (ret)
{
case WAIT_OBJECT_0:
return true;
case WAIT_TIMEOUT:
return false;
case WAIT_ABANDONED:
throw new AbandonedMutexException();
case WAIT_FAILED:
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
default:
return false;
}
}
}