我有一个用 C# 构建的 Excel XLL 插件(使用 ExcelDNA),它执行的任务之一(用户单击按钮时)是调用外部 DLL,该 DLL 执行可能需要几分钟才能完成的数据库请求。 为了避免主 GUI 线程在这段时间内锁定,我将对外部 DLL 的调用包装在 BackgroundWorker 内。 一切似乎都按预期工作,除了现在 Excel 无法正确关闭,我认为是因为后台线程永远不会得到整理?
internal class GetStructure
{
private DataCache cache;
public GetStructure(DataCache cache)
{
this.cache = cache;
}
public void RetrieveStructure()
{ // This is the wrapper for the actual function, as it needs to find and load the Bridging Tool DLLs first
// ... some code to get the inputs and build a string array called request[]
ForceDLLLoad(); // Make sure the assembly is loaded before calling the Aucotec DLL
BackgroundWorker bw = new BackgroundWorker();
bw.WorkerSupportsCancellation = true;
bw.DoWork += new DoWorkEventHandler(bw_DoWork);
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
bw.RunWorkerAsync(request);
bw = null;
}
private void bw_DoWork(object sender, DoWorkEventArgs e)
{ // This is the worker thread code - unpack the argument and call the method that gets data
// Do not attempt to access anything on the main thread in this call
string[] request = (string[])e.Argument;
string[] outputs = this.RetrieveStructureFromEB(request);
e.Result = outputs;
}
private string[] RetrieveStructureFromEB(string[] request)
{ // This method is buried privately because a caller needs to make sure the DLLs are loaded first!
SIMExporterEbmlImport.BridgingTool EB = new BridgingTool();
string[] outputs = EB.GetStructuredData(cache.Workbook.FullName, request);
EB = null;
return outputs;
}
private void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{ // Unpack the results from the worker thread into an Excel sheet
string[] outputs = (string[])e.Result;
if (outputs.Length < 1)
{
MessageBox.Show("Unable to retrieve structure from EB.", "DataTool - Get EB Structure", MessageBoxButtons.OK, MessageBoxIcon.Warning);
Common.ExitToExcel();
return;
}
// ... some code that writes the outputs[] array to an Excel sheet
MessageBox.Show("EB Structure Data has been retrieved and written to the Config sheet.", "DataTool - Get EB Structure", MessageBoxButtons.OK, MessageBoxIcon.Information);
Common.ExitToExcel();
BackgroundWorker worker = (BackgroundWorker)sender;
worker.Dispose(); worker = null;
GC.Collect()
}
代码确实按预期完成执行,因为我得到了在 RunWorkerCompleted 事件中触发的 MessageBox 弹出窗口(成功或失败);我将数据写入 Excel 工作表。问题是,当我尝试关闭 Excel 时,它就变成了僵尸。
如果我尝试关闭 Excel(GUI 消失,但任务管理器保持无头 Excel.exe 在后台运行),我留下的“僵尸”Excel.exe 声称正在等待 splwow64.exe,但我有不知道为什么。
最后,我调用的外部 DLL 发出耗时的请求会消耗大量 RAM (>500 MB) - 我强制使用 GC.Collect() 清除该 RAM,以强制尽快释放该 RAM(这确实如此)。但我的 Excel.exe 仍然无法正常关闭。
Common.ExitToExcel();
有什么作用?
对 Excel COM 对象模型的所有调用都必须在 Excel 主线程上进行。 您确实必须遵循此规则。 (从主线程进行的 COM 对象模型调用可能仍会因各种原因而失败,因为 Excel 有时会“挂起”对象模型。因此,这是顺利进行 COM 调用的必要条件,但不是充分条件。)
您可以通过确认
Thread.CurrentThread.ManagedId == 1
来检查代码是否在Excel主线程上运行。
您的代码可以通过调用 Excel-DNA 辅助方法
ExcelAsyncUtil.QueueAsMacro
从某个工作线程完成“转换”到主线程。您传递给此方法的委托中的代码将始终在主线程的宏上下文中运行,其中对 COM 对象模型(和 C API)的调用可能会起作用,并且 COM 对象不会让您的进程在之后保持活动状态。关闭 Excel。
如果不遵循此规则,您将得到永远没有机会被清理的 COM 对象。那么Excel进程就会变成僵尸等待这些对象被释放。没有可靠的方法来跟踪与其他线程关联的 COM 对象,相反,仅在主线程上使用的 COM 对象不会使 Excel 保持运行。
您没有显示足够的代码来知道这是否是问题所在,但我猜
Common.ExitToExcel()
正在带您到糟糕的地方。