一处消息死锁分析
最近维护一个工控机上运行的winform程序,我的前任在一个弹出窗口(窗口B)里面调用了ShowDialog方法弹出对话框(窗口C),导致了一个问题是有时关闭窗口C时windows假死(无规律),最后用windbg和远程调试找到了问题。解决方法如下:用一个委托来执行ShowDialog。
public delegate DialogResult DelegateShowMessageForm(string msg);
原文地址:http://blog.csdn.net/shuizhilan/article/details/46499939
版权声明:本文为博主原创文章,未经博主允许不得转载。
windows是一个消息驱动的系统,也是个多任务调度系统,windows中的线程分为两类,GUI线程与Worker线程,每个GUI线程会关联消息队列,当消息处理顺序不当时,则有可能造成消息死锁。
使用VS2008打开项目工程,按F5启动调试,该工具工作正常,点击退出按钮,此时会发现该工具失去了响应。按Ctrl+Alt+Break将程序中断,发现程序停在了如下位置。
可以看出,当接收到退出消息时,该函数会被调用,函数内部设置了退出事件,然后等待另一个线程退出,不过看来WaitForSingleObject并没有返回,也就是说另一个线程并没有退出,通过阅读代码得知其执行函数为CXXXXToolDLg::g_ThreadXXXX,调出线程窗口列表,并转到该线程。
查看该线程的调用栈,发现其当前停在了User32.dll!_NtUserMessageCall函数,向下追溯发现,是该线程调用了CListCtrl::GetItemCount,而在GetItemCount中又调用了SendMessage()的原因,看来问题就出现在这个SendMessage了,不过该函数为何不能返回?
查阅MSDN,对该函数的解释如下:
如果SendMessage发送消息的目标窗口是该调用线程自己产生的,那么消息处理函数会类似子程序一样立即得到调用。如果目标窗口是另一线程产生的,系统会切换到接收该消息的线程,然后调用对应的消息处理函数。在线程之间传递的消息只有当接收线程执行获取消息的代码的时候才被处理。发送消息的线程会一直阻塞,直到接收消息的线程处理完成。
这段话不是很好理解,那么就从消息机制的实现来进行分析。
对于每个windows线程,当线程刚被创建时,所有线程都不是GUI线程,只有当线程使用Windows子系统内核服务(win32k.sys)时,Windows才将线程转换为GUI线程。同时,每个GUI线程将关联一个THREADINFO结构,这个结构中包含四个消息队列。
1. Send Message Queue 发送消息队列
2. Posted Message Queue 登记消息队列
3. Visualized Input Queue 输入消息队列
4. Reply Message Queue 响应消息队列
Sent Message Queue: 该队列保存其他程序通过SendMessage给该线程发送的消息
Posted Message Queue: 该队列保存其他队列通过PostMessage给该线程发送的消息
Visualized Input Queue: 保存系统队列分发过来的消息,比如鼠标或者键盘的消息
Reply Message Queue: 保存向窗体发送消息后的结果,比如sendMessage操作结束后,接收消息方会发送一个Reply消息给发送方的Reply队列中,以唤醒发送队列。
每个GUI程序,都有一个消息循环,不断的通过GetMessage从消息队列中取出消息,并用DispatchMessage发送给该消息的消息响应函数,实际上是DispatchMessage调用了该消息的响应函数。
对于SendMessage函数,则有如下两种情形:
1. 当发送消息的目标窗口是调用线程自己创建时:
SendMessage会直接调用该消息的响应函数,即不需要通过消息循环。
2. 当发送消息的目标窗口是另一线程创建时:
SendMessage首先将该消息挂入接收线程的发送消息队列(Send Message Queue),然后将自身阻塞,系统调度至接收线程,并且当接收线程调用操作消息队列的函数时(经过测试:发现GetMessage或PeekMessage都可以),会取出该消息,并直接调用该消息的响应函数(注意,此时并不需要DispatchMessage),当该消息处理完成后,接收线程会发送一个Reply消息给发送方的Reply队列中,以唤醒发送队列。
因此在这个程序中,当点击退出按钮时时,主线程设置退出事件,将自身阻塞,开始等待子线程退出,而子线程则使用了SendMessage向主窗口发送了消息,并只有该消息完成才会返回,此时系统会切换到主线程,但主线程也在等待子线程,没有办法去调用操作消息队列的函数,因此形成了互相等待的局面,即死锁。
改进的方法有很多,但为了不破坏程序原有结构,采用的方法是让主线程既能等待子线程退出,同时也要能处理消息。微软为了解决这一问题,提供了一个API函数,声明如下:
DWORD WINAPI MsgWaitForMultipleObjects(
_In_ DWORD nCount,
_In_ const HANDLE *pHandles,
_In_ BOOL bWaitAll,
_In_ DWORD dwMilliseconds,
_In_ DWORD dwWakeMask
);
MsgWaitForMultipleObjects()函数类似WaitForMultipleObjects(),但它会在“对象被激发”或“消息到达队列”时被唤醒而返回。因此将代码改写为如下形式:
问题得到了解决。
在上图中,可以看到新增了一个消息的处理过程,不过根据上面的描述,在这种情形下,其实只需要增加一个PeekMessage就可以处理发送线程Send过来的消息了,在此这样写是为了保证通用性,即也能处理Post队列中的消息。
另外,也可以利用SendNotifyMessage或SendMessageTimeout来避免出现死锁,在此不再赘述。
最近做winform程序时,在主窗口用线程加了个刷新电量的线程(用于实现充电状态的效果),后面导致其他窗口关闭时假死。用DebugView抓取Debug信息后发现,该窗口的From_Closing事件和Close方法都执行完了,但窗口未关闭。最后将刷新电量的线程取消,改用下文方法,贴上部分代码。
int i = 0; //用于充电时刷新电池图片
private void ChangeBatteryPic(IDModulePower power)
{
if (!currIDModulePower.Equals(power))
{
int picNum = power.QuantityOfBattery;
switch (power.PowerStatus)
{
case IDModulePowerStatus.ExternalPower:
picNum = 7;
RefreshBatteryPic(picNum);
break;
case IDModulePowerStatus.BatteryPower:
RefreshBatteryPic(picNum);
break;
case IDModulePowerStatus.OnCharging:
{
if (i < 6)
{
i++;
}
else
{
i = 0;
}
RefreshBatteryPic(i);
}
break;
case IDModulePowerStatus.ChargeException:
picNum = 6;
RefreshBatteryPic(picNum);
break;
default:
break;
}
currIDModulePower = power;
}
}
public delegate void RefreshControl(int i);
private void RefreshBatteryPic(int picNum)
{
if (this.InvokeRequired)
{
this.BeginInvoke(new RefreshControl(RefreshBatteryPic),picNum);
}
else
{
this.pbBattery.BackgroundImage = VALWELL.SSLC.Resource.Resources.CurrBatteryStatus(picNum);
this.pbBattery.BackgroundImageLayout = ImageLayout.Center;
Application.DoEvents();
}
}
原文地址:http://blog.csdn.net/jianwt/article/details/8128765
引言
基础理论
控件的线程安全检测
Control的Invoke和BeginInvoke
- Control.Invoke,Control.BeginInvoke和delegate.Invoke,delegate.BeginInvoke是不同的。
- Control.Invoke中的委托方法,执行在主线程,也就是我们的UI线程。而Control.BeginInvoke从命名上来看虽然具有异步调用的特征(Begin),但也仍然执行在UI线程。
- 如果在UI线程中直接调用Invoke和BeginInvoke,数据量偏大时,依然会造成UI的假死。
体验BeginInvoke

{
// 储存UI线程的标识符
int curThreadID = Thread.CurrentThread.ManagedThreadId;
new Thread((ThreadStart)delegate()
{
PrintThreadLog(curThreadID);
})
.Start();
}
privatevoid PrintThreadLog(int mainThreadID)
{
// 当前线程的标识符
// A代码块
int asyncThreadID = Thread.CurrentThread.ManagedThreadId;
// 输出当前线程的扼要信息,及与UI线程的引用比对结果
// B代码块
label1.BeginInvoke((MethodInvoker)delegate()
{
// 执行BeginInvoke内的方法的线程标识符
int curThreadID = Thread.CurrentThread.ManagedThreadId;
label1.Text =string.Format("Async Thread ID:{0},Current Thread ID:{1},Is UI Thread:{2}",
asyncThreadID, curThreadID, curThreadID.Equals(mainThreadID));
});
// 挂起当前线程3秒,模拟耗时操作
// C代码块
Thread.Sleep(3000);
}

Control.BeginInvoke的真正含义
Control.Invoke、BeginInvoke与Windows消息
{
using (new MultithreadSafeCallScope())
{
returnthis.FindMarshalingControl().MarshaledInvoke(this, method, args, true);
}
}
public IAsyncResult BeginInvoke(Delegate method, paramsobject[] args)
{
using (new MultithreadSafeCallScope())
{
return (IAsyncResult)this.FindMarshalingControl().MarshaledInvoke(this, method, args, false);
}
}
{
return entry;
}
if (!entry.IsCompleted)
{
this.WaitForWaitHandle(entry.AsyncWaitHandle);
}
Application.DoEvents
解决方案
尝试”无假死”

privatevoid button1_Click(object sender, EventArgs e)
{
new Thread((ThreadStart)(delegate()
{
for (int i =0; i < Max_Item_Count; i++)
{
// 此处警惕值类型装箱造成的"性能陷阱"
listView1.Invoke((MethodInvoker)delegate()
{
listView1.Items.Add(new ListViewItem(newstring[]
{ i.ToString(), string.Format("This is No.{0} item", i.ToString()) }));
});
};
}))
.Start();
}

问题分析
最终方案
- 新建Windows组件DBListView.cs,让它继承自ListView。
- 在控件中添加如下代码:
{
// 打开控件的双缓冲
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
}
{
new Thread((ThreadStart)(delegate()
{
for (int i =0; i < Max_Item_Count; i++)
{
// 此处警惕值类型装箱造成的"性能陷阱"
dbListView1.Invoke((MethodInvoker)delegate()
{
dbListView1.Items.Add(new ListViewItem(newstring[]
{ i.ToString(), string.Format("This is No.{0} item", i.ToString()) }));
});
};
}))
.Start();
}