Excel 中的内存管理

适用于:Excel 2013 | Office 2013 | Visual Studio

如果要创建高效且稳定的 XL,内存管理是最重要的问题。 未能很好地管理内存可能会导致 Microsoft Excel 中出现一系列问题,从内存分配和初始化效率低下、内存泄漏小等小问题,到 Excel 不稳定等重大问题。

内存管理不力是与加载项相关的严重问题的最常见原因。 因此,应使用一致且深思熟虑的内存管理策略生成项目。

随着多线程工作簿重新计算的引入,Microsoft Office Excel 2007 中的内存管理变得更加复杂。 如果要创建和导出线程安全的工作表函数,则必须管理在多个线程争用访问时可能发生的冲突。

以下三种数据结构类型存在内存注意事项:

  • XLOPERXLOPER12
  • 不在 XLOPERXLOPER12中的字符串
  • FPFP12 数组

XLOPER/XLOPER12 内存

XLOPER/ XLOPER12数据结构包含一些子类型,这些子类型包含指向内存块的指针,即 (xltypeStr) 的字符串、xltypeMulti) (数组,以及 xltypeRef) (外部引用。 另请注意, xltypeMulti 数组可以包含字符串 XLOPER/ XLOPER12s ,这些字符串又指向其他内存块。

可以通过多种方式创建 XLOPER/ XLOPER12

  • 在准备要传递给 XLL 函数的参数时由 Excel
  • 在 C API 调用中返回 XLOPERXLOPER12 时由 Excel
  • 创建要传递给 C API 的参数时由 DLL 调用
  • 创建 XLL 函数返回值时由 DLL

可以通过多种方式分配其中一种内存点类型中的内存块:

  • 它可以是 DLL 中任何函数代码之外的静态块,在这种情况下,无需分配或释放内存。
  • 它可以是 DLL 中某些函数代码中的静态块,在这种情况下,无需分配或释放内存。
  • DLL 可以通过几种可能的方式动态分配和释放它: mallocfreenewdelete 等。
  • 它可由 Excel 动态分配。

鉴于 XLOPER/ XLOPER12 内存的可能源的数目以及 XLOPER/ XLOPER12 可能分配了该内存的情况的数量,因此此主题似乎非常困难也就不足为奇了。 但是,如果遵循多个规则和准则,复杂性可能会大大降低。

使用 XLOPER/XLOPER12 的规则

  • 请勿尝试释放内存或覆盖作为参数传递给 XLL 函数的 XLOPER/ XLOPER12。 应将此类参数视为只读参数。 有关详细信息,请参阅 Excel XLL 开发中的已知问题中的“通过就地修改参数返回 XLOPERXLOPER12”。

  • 如果 Excel 已为在调用 C API 时返回到 DLL 的 XLOPER/ XLOPER12 分配内存:

    • 当不再需要 XLOPER/ XLOPER12 时,必须使用调用 xlFree 来释放内存。 请勿使用任何其他方法(例如释放或删除)来释放内存。
    • 如果返回的类型为 xltypeMulti,请不要覆盖数组中的任何 XLOPER/ XLOPER12,尤其是当它们包含字符串时,尤其是当你尝试用字符串覆盖时。
    • 如果要将 XLOPER/ XLOPER12 作为 DLL 函数的返回值返回到 Excel,则必须告知 Excel 在完成后,Excel 必须释放内存。
  • 只能对 XLOPER XLOPER12调用 xlFree ,该 XLOPER/ XLOPER12 创建为 C API 调用的返回值。

  • 如果 DLL 已为要返回到 Excel 的 XLOPER/ XLOPER12 分配内存作为 DLL 函数的返回值,则必须告知 Excel DLL 必须释放内存。

内存管理指南

  • 在用于分配和释放内存的 方法的 DLL 中保持一致。 避免混合方法。 一种好方法是将正在使用的方法包装在内存类或结构中,在内存类或结构中可以更改使用的方法,而无需在很多位置更改代码。
  • 在 DLL 中创建 xltypeMulti 数组时,在为字符串分配内存的方式上保持一致:始终动态分配内存或始终使用静态内存。 如果执行此操作,在释放内存时,将知道必须始终释放字符串或从不释放字符串。
  • 复制 Excel 创建的 XLOPER/ XLOPER12时,创建 Excel 分配的内存的深层副本。
  • 不要将 Excel 分配的字符串 XLOPER/ XLOPER12放在 xltypeMulti 数组中。 创建字符串的深层副本,并在数组中存储指向副本的指针。

释放 Excel-Allocated XLOPER/XLOPER12内存

请考虑以下 XLL 命令,该命令使用 xlGetName 获取包含 DLL 的路径和文件名的字符串,并使用 xlcAlert 将其显示在警报对话框中。

int WINAPI show_DLL_name(void)
{
    XLOPER12 xDllName;
    if(Excel12(xlfGetName, &xDllName, 0) == xlretSuccess)
    {
        // Display the name.
        Excel12(xlcAlert, 0, 1, &xDllName);
        // Free the memory that Excel allocated for the string.
        Excel12(xlFree, 0, 1, &xDllName);
    }
    return 1;
}

当函数不再需要 xDllName 指向的内存时,它可以使用调用 xlFree 函数(仅限 DLL 的 C API 函数之一)来释放它。

xlFree 函数完整记录在函数引用部分 (请参阅只能从 DLL 或 XLL) 调用的 C API 函数,但请注意以下事项:

  • 可以在单个调用 xlFree 中将指针传递到多个 XLOPER/ XLOPER12,仅受 Excel 2003 中运行版本的 Excel (30、从 Excel 2007) 开始的 255 支持的函数参数数限制。
  • xlFree 将包含的指针设置为 NULL ,以确保尝试释放已释放的 XLOPER/ XLOPER12 是安全的。 xlFree 是唯一修改其参数的 C API 函数。
  • 无论它是否包含指向内存的指针,都可以在用于 C API 调用的返回值的任何 XLOPER/ XLOPER12上安全地调用 xlFree

返回由 Excel 释放的 XLOPER/XLOPER12

假设你想要修改上一部分中的示例命令,并将其更改为工作表函数,该工作表函数在传递 布尔true 参数时返回 DLL 路径和文件名,否则 #N/A 。 显然,在将字符串内存返回到 Excel 之前,不能调用 xlFree 来释放字符串内存。 但是,如果在某个时间点未释放它,则每次调用函数时,外接程序都会泄漏内存。 若要解决此问题,可以在 XLOPER/ XLOPER12xltype 字段中设置一个位,在 xlcall.h 中定义为 xlbitXLFree。 设置此项会告知 Excel,在完成复制值后,它必须释放返回的内存。

示例

下面的代码示例演示了上一节中转换为 XLL 工作表函数的 XLL 命令。

LPXLOPER12 WINAPI get_DLL_name(int calculation_trigger)
{
    static XLOPER12 xRtnValue; // Not thread-safe
    Excel12(xlfGetName, &xRtnValue, 0);
// If xlfGetName failed, xRtnValue will be #VALUE!
    if(xRtnValue.xltype == xltypeStr)
    {
// Tell Excel to free the string memory after
// it has copied out the return value.
        xRtnValue.xltype |= xlbitXLFree;
    }
    return &xRtnValue;
}

使用 XLOPER/ XLOPER12的 XLL 函数必须声明为获取和返回指向 XLOPER/ XLOPER12的指针。 在此示例中,在函数中使用静态 XLOPER12 不是线程安全的。 可以将此函数错误地注册为线程安全,但可能会有一个线程在另一个线程完成之前被一个线程覆盖 xRtnValue 的风险。

必须在调用分配它的 Excel 回调后设置 xlbitXLFree 。 如果在此之前设置,则会覆盖它,并且不会产生所需的效果。 如果打算在将该值返回到工作表之前在调用另一个 C API 函数时将该值用作参数,则应在任何此类调用之后设置此位。 否则,在检查 XLOPER/ XLLOPER12 类型之前,将混淆未屏蔽此位的函数。

返回要由 DLL 释放的 XLOPER/XLOPER12s

当 XLL 已为 XLOPER/ XLOPER12 分配内存并希望将其返回到 Excel 时,会出现类似的问题。 Excel 可识别可在 XLOPER/ XLOPER12xltype 字段中设置的另一位,该位在 xlcall.h 中定义为 xlbitDLLFree

当 Excel 收到具有此位集的 XLOPER/ XLOPER12时,它会尝试调用 XLL 导出的函数,该函数应为 XLOPERs () xlAutoFree12 (XLOPER12) 导出。 函数参考中更全面地介绍了此函数 (请参阅 外接程序管理器和 XLL 接口函数) ,但此处提供了一个最小实现示例。 其目的是以与最初分配 XLOPER 的方式一致的方式释放 XLOPER/ XLOPER12 内存。

示例

以下示例函数与上一个函数相同,只不过它在 DLL 名称之前包含文本“此 DLL 的完整路径名为”。

#include <string.h>
LPXLOPER12 WINAPI get_DLL_name_2(int calculation_trigger)
{
    static XLOPER12 xRtnValue; // Not thread-safe
    Excel12(xlfGetName, &xRtnValue, 0);
// If xlfGetName failed, xRtnValue will be #VALUE!
    if(xRtnValue.xltype != xltypeStr)
        return &xRtnValue;
// Make a copy of the DLL path and file name.
    wchar_t *leader = L"The full pathname for this DLL is ";
    size_t leader_len = wcslen(leader);
    size_t dllname_len = xRtnValue.val.str[0];
    size_t msg_len = leader_len + dllname_len;
    wchar_t *msg_text = (wchar_t *)malloc(msg_len + 1);
    wcsncpy_s(msg_text + 1, leader, leader_len);
    wcsncpy_s(msg_text + 1 + leader_len, xRtnValue.val.str + 1,
        dllname_len);
    msg_text[0] = msg_len;
// Now the original string has been copied Excel can free it.
    Excel12(xlFree, 0, 1, &xRtnValue);
// Now reuse the XLOPER12 for the new string.
    xRtnValue.val.str = msg_text;
// Tell Excel to call back into the DLL to free the string
// memory after it has copied out the return value.
    xRtnValue.xltype     = xltypeStr | xlbitDLLFree;
    return &xRtnValue;
}

在 XLL 中导出上一个函数的 xlAutoFree12 的最小足够实现如下所示。

void WINAPI xlAutoFree12(LPXLOPER12 p_oper)
{
    if(p_oper->xltype == (xltypeStr | xlbitDLLFree))
        free(p_oper->val.str);
}

仅当 XLL 仅返回 XLOPER12 字符串,并且仅使用 malloc 分配这些字符串时,此实现才足够。 请注意,测试

if(p_oper->xltype == xltypeStr)

在这种情况下会失败,因为设置了 xlbitDLLFree

通常,应实现 xlAutoFreexlAutoFree12 ,以便释放与 XLL 创建的 xltypeMulti 数组和 xltypeRef 外部引用关联的内存。

你可能决定实现 XLL 函数,以便它们全部返回动态分配的 XLOPERXLOPER12。 在这种情况下,无论子类型如何,都需要在所有此类 XLOPERXLOPER12上设置 xlbitDLLFree。 还需要实现 xlAutoFreexlAutoFree12 ,以便释放此内存以及 XLOPER/ XLOPER12内指向的任何内存。 此方法是确保返回值线程安全的一种方法。 例如,可以按如下所示重写上一个函数。

#include <string.h>
LPXLOPER12 WINAPI get_DLL_name_3(int calculation_trigger)
{
// Thread-safe
    LPXLOPER12 pxRtnValue = (LPXLOPER12)malloc(sizeof(XLOPER12));
    Excel12(xlfGetName, pxRtnValue, 0);
// If xlfGetName failed, pxRtnValue will be #VALUE!
    if(pxRtnValue->xltype != xltypeStr)
    {
// Even though an error type does not point to memory,
// Excel needs to pass this oper to xlAutoFree12 to
// free pxRtnValue itself.
        pxRtnValue->xltype |= xlbitDLLFree;
        return pxRtnValue;
    }
// Make a copy of the DLL path and file name.
    wchar_t *leader = L"The full pathname for this DLL is ";
    size_t leader_len = wcslen(leader);
    size_t dllname_len = pxRtnValue->val.str[0];
    size_t msg_len = leader_len + dllname_len;
    wchar_t *msg_text = (wchar_t *)malloc(msg_len + 1);
    wcsncpy_s(msg_text + 1, leader, leader_len);
    wcsncpy_s(msg_text + 1 + leader_len, pxRtnValue->val.str + 1,
        dllname_len);
    msg_text[0] = msg_len;
// Now the original string has been copied Excel can free it.
    Excel12(xlFree, 0, 1, pxRtnValue);
// Now reuse the XLOPER12 for the new string.
    pxRtnValue->val.str = msg_text;
    pxRtnValue->xltype = xltypeStr | xlbitDLLFree;
    return pxRtnValue;
}
void WINAPI xlAutoFree12(LPXLOPER12 p_oper)
{
    if(p_oper->xltype == (xltypeStr | xlbitDLLFree))
        free(p_oper->val.str);
    free(p_oper);
}

有关 xlAutoFreexlAutoFree12 的详细信息,请参阅 xlAutoFree/xlAutoFree12

返回就地修改参数

Excel 允许 XLL 函数通过就地修改参数来返回值。 只能使用作为指针传入的参数来执行此操作。 若要像这样工作,必须以告知 Excel 将修改哪个参数的方式注册函数。

所有可通过指针传递的数据类型都支持这种返回值的方法,但对于以下类型尤其有用:

  • 长度计数和以 null 结尾的 ASCII 字节字符串
  • 从 Excel 2007 开始的长度计数和以 null 结尾的 Unicode 宽字符字符串 ()
  • FP 浮点数组
  • 从 Excel 2007 开始 (FP12 浮点数组)

注意

不应尝试以这种方式返回 XLOPERXLOPER12。 有关详细信息,请参阅 Excel XLL 开发中的已知问题

使用此方法(而不仅仅是使用 return 语句)的优点是 Excel 为返回值分配内存。 Excel 读取完返回的数据后,会释放内存。 这会将内存管理任务从 XLL 函数中移开。 此方法是线程安全的:如果 Excel 在不同线程上并发调用,则每个线程上的每个函数调用都有自己的缓冲区。

它尤其适用于前面列出的数据类型,因为对于简单字符串和 FPFP12/ 数组,XLOPER/ XLOPER12不存在用于回叫 DLL 以释放内存后返回内存的机制。 因此,返回 DLL 创建的字符串或浮点数组时,有以下选择:

  • 将持久指针设置为动态分配的缓冲区,返回该指针。 下一次调用函数时 (1) 检查 指针不为 null, (2) 释放在上一次调用上分配的资源,并将指针重置为 null, (3) 为新分配的内存块重用指针。
  • 在不需要释放的静态缓冲区中创建字符串和数组,并返回指向该缓冲区的指针。
  • 就地修改参数,将字符串或数组直接写入 Excel 留出的空间。

否则,必须创建 XLOPER/ XLOPER12,并使用 xlbitDLLFreexlAutoFree/ xlAutoFree12 释放资源。

在传递与返回值类型相同的参数的情况下,最后一个选项可能是最简单的。 要记住的要点是缓冲区大小有限,必须非常小心不要溢出缓冲区大小。 出现此错误可能会使 Excel 崩溃。 接下来将讨论字符串和 FP/ FP12 数组的缓冲区大小。

字符串

字符串内存管理问题可以说是应用程序和加载项不稳定的最常见原因。鉴于字符串的处理方式多种多样,这也许是可以理解的:以 null 结尾或长度计数的 (或两者) ;静态或动态缓冲区;固定长度或几乎无限长度;操作系统托管内存 (例如,OLE Bstr) 或非托管字符串;等等。

C/C++ 程序员最熟悉以 null 结尾的字符串。 标准 C 库旨在处理此类字符串。 代码中的静态字符串文本编译为以 null 结尾的字符串。 或者,Excel 适用于通常不以 null 结尾的长度计数字符串。 这些事实的组合需要 DLL/XLL 中关于如何处理字符串和字符串内存的明确且一致的方法。

最常见的问题如下:

  • 将 null 或无效指针传递给需要有效指针且不检查指针本身的有效性的函数。
  • 不或不能检查所写入字符串长度的缓冲区长度的函数对字符串缓冲区边界的溢出。
  • 尝试释放静态、已释放或未以与释放方式一致的方式分配的字符串缓冲区内存。
  • 内存泄漏,导致字符串被分配,然后未释放,通常在经常调用的函数中。

字符串规则

XLOPER/ XLOPER一样,应遵循一些规则和准则。 这些准则与上一节中给出的相同。 此处的规则是专门针对字符串的规则的扩展。

规则:

  • 请勿尝试释放内存或覆盖字符串 XLOPER/ XLOPER12或作为参数传递给 XLL 函数的简单长度计数或以 null 结尾的字符串。 应将此类参数视为只读参数。
  • 其中,Excel 为 C API 回调函数的返回值分配字符串 XLOPER/ XLOPER12 内存,使用 xlFree 释放它,或者如果从 XLL 函数将其返回到 Excel,则设置 xlbitXLFree
  • 如果 DLL 为 XLOPER/ XLOPER12动态分配字符串缓冲区,请以与完成后分配它的方式一致的方式释放它;如果从 XLL 函数将它返回到 Excel,则设置 xlbitDLLFree ,然后在 xlAutoFree/ xlAutoFree12 中释放它。
  • 如果 Excel 已为在调用 C API 时返回到 DLL 的 xltypeMulti 数组分配内存,请不要覆盖数组中的任何字符串 XLOPER/ XLOPER12。 此类数组只能使用 xlFree 释放,或者,如果 XLL 函数返回,则通过设置 xlbitXLFree 来释放

支持的字符串类型

C API xltypeStr XLOPER/XLOPER12s

字节字符串: XLOPER 宽字符字符串: XLOPER12
Excel 的所有版本 自 Excel 2007 起
最大长度:255 个扩展的 ASCII 字节 最大长度 32,767 Unicode 字符
第一个 (无符号) 字节 = 长度 第一个 Unicode 字符 = 长度

重要

不要假设 XLOPERXLOPER12 字符串的空终止。

C/C++ 字符串

字节字符串 宽字符字符串
以 null 结尾的 (字符 *) “C”最大长度:255 个扩展 ASCII 字节 以 null 结尾的 (wchar_t *) “C%”最大长度 32,767 Unicode 字符
长度计数 (无符号字符 *) “D” 长度计数 (wchar_t *) “D%”

xltypeMulti XLOPER/XLOPER12 数组中的字符串

在某些情况下,Excel 会创建 一个 xltypeMulti 数组,以便在 DLL/XLL 中使用。 多个 XLM 信息函数返回此类数组。 例如,C API 函数 xlfGetWorkspace 在传递参数 44 时返回一个数组,其中包含描述所有当前注册的 DLL 过程的字符串。 C API 函数 xlfDialogBox 返回其数组参数的修改副本,其中包含字符串的深层副本。 XLL 遇到 xltypeMulti 数组的最常见方式可能是,它已作为参数传递给 XLL 函数,或者它已从范围引用强制转换为此类型。 在后一种情况下,Excel 会在源单元格中创建字符串的深层副本,并指向数组中的这些字符串。

如果要在 DLL 中修改这些字符串,则应创建自己的深层副本。 创建自己的 xltypeMulti 数组时,不应将 Excel 分配的字符串 XLOPER/ XLOPER12放在其中。 这可能导致你以后无法正确释放它们,或者根本无法释放它们。 同样,应创建字符串的深层副本,并在数组中存储指向副本的指针。

示例

以下示例函数创建长度计数的 Unicode 字符串的动态分配副本。 请注意,调用方最终必须使用 delete[] 释放此示例中分配的内存,并且不假定源字符串以 null 结尾。 如果出于安全原因,复制字符串将被截断,并且不会以 null 结尾。

#include <string.h>
#define MAX_V12_STRBUFFLEN    32678
    
wchar_t * deep_copy_wcs(const wchar_t *p_source)
{
    if(!p_source)
        return NULL;
    size_t source_len = p_source[0];
    bool truncated = false;
    if(source_len >= MAX_V12_STRBUFFLEN)
    {
        source_len = MAX_V12_STRBUFFLEN - 1; // Truncate the copy
        truncated = true;
    }
    wchar_t *p_copy = new wchar_t[source_len + 1];
    wcsncpy_s(p_copy, p_source, source_len + 1);
    if(truncated)
        p_copy[0] = source_len;
    return p_copy;
}

然后,此函数可以安全地用于复制 XLOPER12,如以下可导出的 XLL 函数所示,该函数返回其参数的副本(如果它是一个字符串)。 所有其他类型都以零长度字符串的形式返回。 请注意,不处理范围 - 函数返回 #VALUE!。 函数必须注册为采用 U 类型参数,以便作为值传入引用。 这等效于内置工作表函数 T () 不同, AsText 还会将错误转换为零长度字符串。 此代码示例假定 xlAutoFree12 使用 delete 释放传入的指针及其内容。

LPXLOPER12 WINAPI AsText(LPXLOPER12 pArg)
{
    LPXLOPER12 pRtnVal = new XLOPER12;
// If the input was an array, only operate on the top-left element.
    LPXLOPER *pTemp;
    if(pArg->xltype == xltypeMulti)
        pTemp = pArg->val.array.lparray;
    else
        pTemp = pArg;
    switch(pTemp->xltype)
    {
        case xltypeErr:
        case xltypeNum:
        case xltypeMissing:
        case xltypeNil:
        case xltypeBool:
            pRtnVal->xltype = xltypeStr | xlbitDLLFree;
            pRtnVal->val.str = deep_copy_wcs(L"\000");
            return pRtnVal;
        case xltypeStr:
            pRtnVal->xltype = xltypeStr | xlbitDLLFree;
            pRtnVal->val.str = deep_copy_wcs(pTemp->val.str);
            return pRtnVal;
        
        default: // xltypeSRef, xltypeRef, xltypeFlow, xltypeInt
            pRtnVal->xltype = xltypeErr | xlbitDLLFree;
            pRtnVal->val.err = xlerrValue;
            return pRtnVal;
    }
}

返回就地修改字符串参数

可以就地修改注册为 类型 FGF%G% 的参数。 当 Excel 为这些类型准备字符串参数时,它会创建最大长度缓冲区。 然后,它将参数字符串复制到该字符串中,即使此字符串非常短。 这使 XLL 函数能够将其返回值直接写入同一内存中。

为这些类型设置不同的缓冲区大小如下:

  • 字节字符串: 256 字节,包括长度计数器 (类型 G) 或 null 终止 (类型 F) 。
  • Unicode 字符串: 32,768 个宽字符 (65,536 字节) 包括长度计数器 (类型 G%) 或 null 终止 (类型 F%) 。

注意

无法直接从 Visual Basic for Applications (VBA) 调用此类函数,因为无法确保已分配足够大的缓冲区。 只有在显式传递了足够大的缓冲区后,才能安全地从另一个 DLL 调用此类函数。

下面是一个 XLL 函数示例,该函数使用标准库函数 wcsrev 反转传入的以 null 结尾的宽字符字符串。 在这种情况下,参数将注册为类型 F%。

void WINAPI reverse_text_xl12(wchar_t *text)
{
    _wcsrev(text);
}

持久性存储 (二进制名称)

二进制名称定义并与二进制块相关联,即与工作簿一起存储的非结构化数据。 它们是使用函数 xlDefineBinaryName 创建的,并且使用函数 xlGetBinaryName 检索数据。 函数参考中更详细地介绍了这两个函数 (请参阅 仅可从 DLL 或 XLL) 调用的 C API 函数 ,并且都使用 xltypeBigDataXLOPER/ XLOPER12

有关限制二进制名称实际应用的已知问题的信息,请参阅 Excel XLL 开发中的已知问题

Excel Stack

Excel 与其加载的所有 DLL 共享其堆栈空间。 堆栈空间通常足以正常使用,只要遵循以下几个准则,就无需关注它:

  • 不要按堆栈上的值将非常大的结构作为参数传递给函数。 请改为传递指针或引用。
  • 不要在堆栈上返回大型结构。 返回指向静态或动态分配的内存的指针,或使用通过引用传递的参数。
  • 不要在函数代码中声明非常大的自动变量结构。 如果需要它们,请将其声明为静态。
  • 除非确定递归深度始终为浅层,否则不要递归调用函数。 请尝试改用循环。

当 DLL 使用 C API 调用回 Excel 时,Excel 首先检查堆栈上是否有足够的空间进行最坏情况的使用调用。 如果它认为空间可能不足,它将使调用无法安全,即使实际上可能有足够的空间用于该特定调用。 在这种情况下,回调返回代码 xlretFailed。 对于 C API 和堆栈的正常使用,这不太可能导致 C API 调用失败。

如果你担心或只是好奇,或者你希望消除堆栈空间不足作为无法解释故障的原因,可以通过调用 xlStack 函数来找出有多少堆栈空间。

另请参阅

Excel 中的多线程重新计算
Excel 中的多线程处理和内存争用
开发 Excel XLL