我有使用来自网络应用程序的 win32 api 的代码。当我在 ASP.Net 开发服务器中运行此代码时,我遇到了死锁(我无法在 IIS 中重现,但我不知道在某些情况下它不会发生)。下面是一个我已经修剪过的类,它仍然重现了这个问题:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.InteropServices;
namespace Web_ShellIconBug
{
public class IconIndexClass
{
[StructLayout(LayoutKind.Sequential)]
private struct SHFILEINFO
{
public IntPtr hIcon;
public int iIcon;
public int dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
public string szTypeName;
}
[DllImport("shell32", CharSet = CharSet.Unicode)]
private static extern IntPtr SHGetFileInfo(string pszPath, int dwFileAttributes, ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags);
private static object m_lock = new object();
public int IconIndex(
string fileName,
bool tryDisk,
int iconState
)
{
// On some machines, you might need this to make sure multiple threads are spawned
//System.Threading.Thread.Sleep(100);
SHFILEINFO shfi = new SHFILEINFO();
IntPtr retVal;
uint shfiSize = (uint)Marshal.SizeOf(shfi.GetType());
MyLog("Before Lock.");
lock (m_lock)
{
MyLog("Obtained Lock.");
retVal = SHGetFileInfo(fileName, 0, ref shfi, shfiSize, 0);
}
MyLog("Lock released.");
if (retVal.Equals(IntPtr.Zero))
{
MyLog("IntPtr is zero");
if (tryDisk)
{
if (System.IO.Directory.Exists(fileName))
return IconIndex(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), false, iconState);
else return IconIndex(fileName, false, iconState);
}
else
return 0;
}
else
{
return shfi.iIcon;
}
}
private void MyLog(string val)
{
System.Diagnostics.Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.ffff") + " - Thread:" + System.Threading.Thread.CurrentThread.ManagedThreadId + " - Msg:" + val);
}
}
}
我可以使用以下代码在 Web 应用程序中重现该错误:
protected void Page_Load(object sender, EventArgs e)
{
Web_ShellIconBug.IconIndexClass ii = new Web_ShellIconBug.IconIndexClass();
Parallel.ForEach(System.IO.Directory.GetFiles("C:\\Windows"), file =>
{
ii.IconIndex(file, false, 0);
});
Debug.WriteLine("Done.");
}
我已经在运行 Win 7 64 位和 VS 2010 SP1 的两台不同的机器上重现了这个。在我的输出中,我将看到如下内容:
21:39:01.7812 - Thread:5 - Msg:Before Lock.
21:39:01.7912 - Thread:5 - Msg:Obtained Lock.
21:39:01.8022 - Thread:5 - Msg:Lock released.
21:39:01.8162 - Thread:10 - Msg:Before Lock.
21:39:02.8382 - Thread:11 - Msg:Before Lock.
21:39:03.8172 - Thread:12 - Msg:Before Lock.
21:39:04.3032 - Thread:5 - Msg:Before Lock.
21:39:04.3032 - Thread:5 - Msg:Obtained Lock.
21:39:04.3042 - Thread:5 - Msg:Lock released.
21:39:04.8162 - Thread:13 - Msg:Before Lock.
...
在这种情况下,看起来线程 5 正在获取锁,但没有释放它,因此所有其他线程都被无限期地阻塞。
其他一些需要注意的事项:
- 重现僵局是相当棘手的。如果我在检查返回值等于 IntPtr.Zero 之后修改任何递归调用,死锁似乎消失了,但我不明白为什么这会影响任何锁定,所以我犹豫要不要说修改该代码纠正问题。
- 如果我手动执行 Monitor.Enter 和 Monitor.Exit (而不是锁定),我不会遇到死锁,但同样,我不确定我是否已经解决了问题,或者只是为我的测试用例修复了它。
- 这段代码与代码的生产版本相比非常精简,因此该类中任何看起来没有多大作用的代码可能是因为我试图从问题中消除尽可能多的噪音,同时仍然能够重新创建它。
任何人都可以提供任何可能导致僵局的见解吗?我似乎无法将手指放在它上面。