Excel 在多线程 C# 应用程序中使用互操作的文件操作失败
Excel file operations using interop in multithreaded C# application fails
我有一个应用程序可以自动执行一些与文件相关的工作。每个作业都在单独的线程中执行。一种工作是将 Excel 文件导出为 HTML 格式。为此,我使用 Microsoft.Office.Interop.Excel
命名空间。我的应用程序在 Windows Server 2008 环境下运行良好,但我们将服务器升级到 Windows Server 2012,我开始收到以下错误:
The message filter indicated that the application is busy. (Exception from HRESULT: 0x8001010A (RPC_E_SERVERCALL_RETRYLATER))
问题是第一次调用导出函数成功地将 Excel 文件导出到 HTML 但后续调用失败并出现上述错误。我确保关闭并完成所有与 Excel 相关的对象,并从任务管理器中检查 excel.exe 是否不工作但没有运气。
如果出现此错误,我使用以下代码重试,但它一直出现异常并在重试 5 次后失败
while (!success)
{
try
{
ExportExcel();
success = true;
System.Threading.Thread.Sleep(2000);
}
catch (System.Runtime.InteropServices.COMException loE)
{
tryCount++;
if (loE.HResult.ToString("X") == "80010001" || loE.HResult.ToString("X") == "8001010A" && tryCount<5)
{
System.Threading.Thread.Sleep(2000);
}
else
{
throw;
}
}
}
我怀疑这可能与某些线程错误有关,但我无法给出答案。任何见解都会有所帮助。
谢谢 Joe 指出正确的方法:
我最终使用了一个混合了以下链接的解决方案:
http://blogs.artinsoft.net/Mrojas/archive/2012/09/28/Office-Interop-and-Call-was-rejected-by-callee.aspx
http://blogs.msdn.com/b/pfxteam/archive/2010/04/07/9990421.aspx
所以我使用了类似下面的东西:
StaTaskScheduler cts=new StaTaskScheduler(1);
TaskFactory factory;
factory = new TaskFactory(cts);
Task jobRunTask = factory.StartNew(() =>
{
MessageFilter.Register();
ExcelInteropFunction();
MessageFilter.Revove();
});
我相信 Excel 对象模型是单元线程的,因此来自多个线程的调用将被编组到 Excel 进程中的同一个线程 - 这可能很忙,特别是如果有多个线程客户端线程。
您可以实现 IMessageFilter(OLE 消息过滤器,不要与 System.Windows.Forms.IMessageFilter
混淆)以提供自定义重试逻辑。
您的服务器升级可能改变了计时特性,因此问题发生的频率更高。
更新
以下是 OLE 消息过滤器的基本实现示例:
// Definition of the IMessageFilter interface which we need to implement and
// register with the CoRegisterMessageFilter API.
[ComImport(), Guid("00000016-0000-0000-C000-000000000046"), InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
interface IOleMessageFilter // Renamed to avoid confusion w/ System.Windows.Forms.IMessageFilter
{
[PreserveSig]
int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo);
[PreserveSig]
int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType);
[PreserveSig]
int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType);
}
internal sealed class OleMessageFilter : IOleMessageFilter, IDisposable
{
[DllImport("ole32.dll")]
private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter);
private bool _isRegistered;
private IOleMessageFilter _oldFilter;
public OleMessageFilter()
{
Register();
}
private void Register()
{
// CoRegisterMessageFilter is only supported on an STA thread. This will throw an exception
// if we can't switch to STA
Thread.CurrentThread.SetApartmentState(ApartmentState.STA);
int result = CoRegisterMessageFilter(this, out _oldFilter);
if (result != 0)
{
throw new COMException("CoRegisterMessageFilter failed", result);
}
_isRegistered = true;
}
private void Revoke()
{
if (_isRegistered)
{
IOleMessageFilter revokedFilter;
CoRegisterMessageFilter(_oldFilter, out revokedFilter);
_oldFilter = null;
_isRegistered = false;
}
}
#region IDisposable Members
private void Dispose(bool disposing)
{
if (disposing)
{
// Dispose managed resources
}
// Dispose unmanaged resources
Revoke();
}
void IDisposable.Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
~OleMessageFilter()
{
Dispose(false);
}
#endregion
#region IOleMessageFilter Members
int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo)
{
return 0; //SERVERCALL_ISHANDLED
}
int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType)
{
if (dwRejectType == 2) // SERVERCALL_RETRYLATER
{
return 200; // wait 200ms and try again
}
return -1; // cancel call
}
int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType)
{
return 2; //PENDINGMSG_WAITDEFPROCESS
}
#endregion
}
您也可以查看 this sample,尽管它会显示提示询问用户是否要重试,如果您的客户端是多线程且基于服务器的,这可能不合适。
我有一个应用程序可以自动执行一些与文件相关的工作。每个作业都在单独的线程中执行。一种工作是将 Excel 文件导出为 HTML 格式。为此,我使用 Microsoft.Office.Interop.Excel
命名空间。我的应用程序在 Windows Server 2008 环境下运行良好,但我们将服务器升级到 Windows Server 2012,我开始收到以下错误:
The message filter indicated that the application is busy. (Exception from HRESULT: 0x8001010A (RPC_E_SERVERCALL_RETRYLATER))
问题是第一次调用导出函数成功地将 Excel 文件导出到 HTML 但后续调用失败并出现上述错误。我确保关闭并完成所有与 Excel 相关的对象,并从任务管理器中检查 excel.exe 是否不工作但没有运气。
如果出现此错误,我使用以下代码重试,但它一直出现异常并在重试 5 次后失败
while (!success)
{
try
{
ExportExcel();
success = true;
System.Threading.Thread.Sleep(2000);
}
catch (System.Runtime.InteropServices.COMException loE)
{
tryCount++;
if (loE.HResult.ToString("X") == "80010001" || loE.HResult.ToString("X") == "8001010A" && tryCount<5)
{
System.Threading.Thread.Sleep(2000);
}
else
{
throw;
}
}
}
我怀疑这可能与某些线程错误有关,但我无法给出答案。任何见解都会有所帮助。
谢谢 Joe 指出正确的方法:
我最终使用了一个混合了以下链接的解决方案: http://blogs.artinsoft.net/Mrojas/archive/2012/09/28/Office-Interop-and-Call-was-rejected-by-callee.aspx
http://blogs.msdn.com/b/pfxteam/archive/2010/04/07/9990421.aspx
所以我使用了类似下面的东西:
StaTaskScheduler cts=new StaTaskScheduler(1);
TaskFactory factory;
factory = new TaskFactory(cts);
Task jobRunTask = factory.StartNew(() =>
{
MessageFilter.Register();
ExcelInteropFunction();
MessageFilter.Revove();
});
我相信 Excel 对象模型是单元线程的,因此来自多个线程的调用将被编组到 Excel 进程中的同一个线程 - 这可能很忙,特别是如果有多个线程客户端线程。
您可以实现 IMessageFilter(OLE 消息过滤器,不要与 System.Windows.Forms.IMessageFilter
混淆)以提供自定义重试逻辑。
您的服务器升级可能改变了计时特性,因此问题发生的频率更高。
更新
以下是 OLE 消息过滤器的基本实现示例:
// Definition of the IMessageFilter interface which we need to implement and
// register with the CoRegisterMessageFilter API.
[ComImport(), Guid("00000016-0000-0000-C000-000000000046"), InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
interface IOleMessageFilter // Renamed to avoid confusion w/ System.Windows.Forms.IMessageFilter
{
[PreserveSig]
int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo);
[PreserveSig]
int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType);
[PreserveSig]
int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType);
}
internal sealed class OleMessageFilter : IOleMessageFilter, IDisposable
{
[DllImport("ole32.dll")]
private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter);
private bool _isRegistered;
private IOleMessageFilter _oldFilter;
public OleMessageFilter()
{
Register();
}
private void Register()
{
// CoRegisterMessageFilter is only supported on an STA thread. This will throw an exception
// if we can't switch to STA
Thread.CurrentThread.SetApartmentState(ApartmentState.STA);
int result = CoRegisterMessageFilter(this, out _oldFilter);
if (result != 0)
{
throw new COMException("CoRegisterMessageFilter failed", result);
}
_isRegistered = true;
}
private void Revoke()
{
if (_isRegistered)
{
IOleMessageFilter revokedFilter;
CoRegisterMessageFilter(_oldFilter, out revokedFilter);
_oldFilter = null;
_isRegistered = false;
}
}
#region IDisposable Members
private void Dispose(bool disposing)
{
if (disposing)
{
// Dispose managed resources
}
// Dispose unmanaged resources
Revoke();
}
void IDisposable.Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
~OleMessageFilter()
{
Dispose(false);
}
#endregion
#region IOleMessageFilter Members
int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo)
{
return 0; //SERVERCALL_ISHANDLED
}
int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType)
{
if (dwRejectType == 2) // SERVERCALL_RETRYLATER
{
return 200; // wait 200ms and try again
}
return -1; // cancel call
}
int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType)
{
return 2; //PENDINGMSG_WAITDEFPROCESS
}
#endregion
}
您也可以查看 this sample,尽管它会显示提示询问用户是否要重试,如果您的客户端是多线程且基于服务器的,这可能不合适。