使用 XLL 技术开发 Excel 插件时的,多线程技术。

线程安全函数

Excel2007 中的大多数工作表函数都是线程安全的。你也可以自己创建线程安全函数。Excel 2007 使用单线程调用“Excel命令”和“非线程安全函数”、“xlAuto 函数”(除了 xlAutoFree 和 xlAutoFree12)、“COM”、“VBA函数”。

当一个XLL函数返回 使用了 xlbitDLLFree 选项的 XLOPER 或 XLOPER12 时,Excel使用调用XLL函数的同一线程去访问 xlAtuoFree或 xlAutoFree12函数。这些调用会在同一线程下次函数调用之前执行。

对于XLL开发人员来说,创建线程安全函数,有如下好处:

  • 可以充分发利用多处理器或多核计算机的计算能力
  • 相对单线程,多线程能更有效的使用远程服务。

假设你使用的是单处理器计算机,设置并打了多线程处理选项,线程数为N。假设,电子表格软件使用XLL函数处理了大量的单元格数据,它依次发送数据请求或远程服务请求。受限于依赖树拓扑结构,Excel 几乎会同时调用此函数N次。多线程使计算过程足够快速以及拥有并发处理能力,电子表的重计算时间可能会因此减少到 1/N。

使用多线程关键点是需要处理好内存的争用问题。这往往意味着,内存争用包含了下面两个问题:

  • 如果创建一个只能由这一个线程使用的内存。
  • 如何确保同享内存的多线程访问安全性问题。

我们首先需要注意的是那些内存是可以被 所有线程 访问的,那些内存是只能被 当前线程 访问的。

所有线程访问的内存

  • 声明在函数体外的 变量、结构、类实例
  • 函数体内的静态变量

以上两个种情况,都是为某一个DLL实例创建的内存块,如果其它的应用程序载入这个DLL,就会另外创建内存,所以两个DLL实例之间不会发生内存争用的问题。

当前线程访问的内存

  • 函数代码内的自动内存

这种情况下,内存在每个函数实例的堆栈上。

单线程内存访问:线程本地内存

考虑到函数体内的静态内存,可以被所有线程访问,此函数显示不是线程安全的。一个线程上的一个函数实例,可以改变这个值,而另外一个线程上的另一个函数会假设它是完全不同的东西。

有两个理由让我们需要在函数体内设置静态变量:

  1. 需要在多次调用之间保存数值。
  2. 这个函数可以安全的返回静态数据指针。

对于第一个原因,你可能需要为所有的函数调用保留数据:比如一个调用计数器,每次调用计数器都会加1,或是收集每次调用函数性能数据。这里需要关注的问题是如何 保护共享数据或数据结构。最好的作法是使用下节介绍的临界段。

如果数据只打算用于单个线程,存在的问题是如何保留一个只能由单线程访问的数据。一种解决方案是使用线程本地存储方案 (TLS thread-local storage)API。

以下实例中,函数返回了一个 XLOPER 指针。

LPXLOPER12 WINAPI mtr_unsafe_example(LPXLOPER12 pxArg)
{
    static XLOPER12 xRetVal; // 内存共享于所有线程!!!
    // code sets xRetVal to a function of pxArg ...
    return &xRetVal;
}

这个函数不是线程安全的,因为当一个线程返回 XLOPER12 时,另外一个线程会再次返回并重写原有值。如果需要传递 XLOPER12 给 xlAutoFree12,发生重写的可能性是很大的。一种解决方法是为每个线程都分配一个 XLOPER12 ,并在实现 xlAutoFree12 时使用 XLOPER12 的实例指针,这样就只会释放自己实例。在我们提供的实例中很多时候都是使用的这种方法。

LPXLOPER12 WINAPI mtr_safe_example_1(LPXLOPER12 pxArg)
{
// pxRetVal must be freed later by xlAutoFree12
    LPXLOPER12 pxRetVal = new XLOPER12;
// code sets pxRetVal to a function of pxArg ...
    pxRetVal->xltype |= xlbitDLLFree; // Needed for all types
    return pxRetVal; // xlAutoFree12 must free this
}

上面这种方法比下一节我们介绍的方法容易些。它依赖于 TLS API。但它也有一些缺点。首先,无论返回的那种 XLOPER/XLOPER12 类型,Excel 必需调用 xlAutoFree/xlAutoFree12 来释放内存,其次,当调用C API回调函数返回XLOPER/XLOPER12s,也会存在一些问题。XLOPER/XLOPER12 指向的内存可能需要由Excel释放,但是 XLOPER/XLOPER12 通常必需使用当初分配内存时,相对应的内存释放方式来释放内存空间 。如果这类 XLOPER/XLOPER12 用作 XLL 工作表函数的返回值,那么就没有合适的方法去通知 xlAutoFree/xlAutoFree12 使用合适的方法释放 它们。为了解决这一问题,XLL 可以使用深拷贝 Excel分配的 XLOPER/XLOPER12s 。

以个避免以上问题的方法是填入和返回本地线程 XLOPER/XLOPER12,这一方法 xlAutoFree/xlAutoFree12 ,而不用释放 XLOPER/XLOPER12 指针。

LPXLOPER12 get_thread_local_xloper12(void);

LPXLOPER12 WINAPI mtr_safe_example_2(LPXLOPER12 pxArg)
{
    LPXLOPER12 pxRetVal = get_thread_local_xloper12();
// Code sets pxRetVal to a function of pxArg setting xlbitDLLFree or
// xlbitXLFree as required.
    return pxRetVal; // xlAutoFree12 must not free this pointer!
}

下一个问题是如果设置和获取线程局部内存,换句话说,如何实现上一节实例中的 get_thread_local_xloper12 函数。这里通过使用 线程本地存储API(TLS)。第一步是使用 TlsAlloc 获取 TLS 索引,它必需使用 TlsFree 释放。这两个函数最好都在 DllMain 中执行。

// This implementation just calls a function to set up
// thread-local storage.
BOOL TLS_Action(DWORD Reason); // Could be in another module

BOOL WINAPI DllMain(HINSTANCE hDll, DWORD Reason, void *Reserved)
{
    return TLS_Action(Reason);
}

DWORD TlsIndex; // Module scope only if all TLS access in this module

BOOL TLS_Action(DWORD DllMainCallReason)
{
    switch (DllMainCallReason)
    {
    case DLL_PROCESS_ATTACH: // The DLL is being loaded.
        if((TlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
            return FALSE;
        break;

    case DLL_PROCESS_DETACH: // The DLL is being unloaded.
        TlsFree(TlsIndex); // Release the TLS index.
        break;
    }
    return TRUE;
}

在你获取 索引后,下一步是为每一个线程分配内存块。 Windows SDK Dynamic-Link Library Reference 中推荐的方法是使用 DLL_THREAD_ATTACH 。使用 DLL_THREAD_DETACH 释放。然而这个建议会导致你的DLL执行的一些操作和 计算是没有关系的。

替代它的最好方法是在第一次使用时分配内存。首先你需要为每个线程定义一个结构。对于上一个例子中中返回的 XLOPER 或 XLOPER12 使用下面的方法就足够了。但你可以创建其他的结构,满足你的需要。

struct TLS_data
{
    XLOPER xloper_shared_ret_val;
    XLOPER12 xloper12_shared_ret_val;
// Add other required thread-local data here...
};

下一个函数获取一个本地实例的指针,或第一次调用时分配一个。

TLS_data *get_TLS_data(void)
{
// Get a pointer to this thread's static memory.
    void *pTLS = TlsGetValue(TlsIndex);
    if(!pTLS) // No TLS memory for this thread yet
    {
        if((pTLS = calloc(1, sizeof(TLS_data))) == NULL)
        // Display some error message (omitted).
            return NULL;
        TlsSetValue(TlsIndex, pTLS); // Associate with this thread
    }
    return (TLS_data *)pTLS;
}

现在你可以看到,线程本地 XLOPER/XLOPER12 是如何获取的:首先你获取一个指向 TLS_data 结构指针,然后,你返回的 XLOPER /XLOPER12 包含它。

LPXLOPER get_thread_local_xloper(void)
{
    TLS_data *pTLS = get_TLS_data();
    if(pTLS)
        return &(pTLS->xloper_shared_ret_val);
    return NULL;
}

LPXLOPER12 get_thread_local_xloper12(void)
{
    TLS_data *pTLS = get_TLS_data();
    if(pTLS)
        return &(pTLS->xloper12_shared_ret_val);
    return NULL;
}

mtr_safe_example_1 和 mtr_safe_example_2 当你运行 Excel 2007 时可以注册为线程安全函数。然后,你不能在一个XLL中混合两种广场。你的XLL只能输出xlAutoFree 和 xlAutoFree12 的一种实现,每个内存管理方式都需要不同的方法。 mtr_safe_example_1 指针传递给 xlAutoFree/xlAutoFree12,必需释放任何指针它的指针。而 mtr_safe_example_2 只有指向的数据结构需要释放。

Windows 还支持函数 GetCurrentThreadId ,它可以返回当前线程唯一ID,支持开发者使用其它的方法编码线程安全函数,或特定的线程行为。

内存由多个线程访问处理方法:临界段

你可以使用临界段来解决多个线程争用内存的情况。你需要为每个内存块临界段进行命名,这些操作你可以在调用函数 xlAutoOpen 时执行,在调用函数 xlAutoClose 时释放它们。然后,你需要在调用EnterCriticalSection 或 LeaveCriticalSection 包含每个访问的被保护的内存块。一次只能允许一个线程访问被保护的内存块。

CRITICAL_SECTION g_csSharedTable; // global scope (if required)
bool xll_initialised = false; // Only module scope needed

int WINAPI xlAutoOpen(void)
{
    if(xll_initialised)
        return 1;
// Other initialisation omitted
    InitializeCriticalSection(&g_csSharedTable);
    xll_initialised = true;
    return 1;
}

int WINAPI xlAutoClose(void)
{
    if(!xll_initialised)
        return 1;
// Other cleaning up omitted.
    DeleteCriticalSection(&g_csSharedTable);
    xll_initialised = false;
    return 1;
}

#define SHARED_TABLE_SIZE 1000 /* Some value consistent with the table */

bool read_shared_table_element(unsigned int index, double &d)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTable);
    d = shared_table[index];
    LeaveCriticalSection(&g_csSharedTable);
    return true;
}

bool set_shared_table_element(unsigned int index, double d)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTable);
    shared_table[index] = d;
    LeaveCriticalSection(&g_csSharedTable);
    return true;
}

另外,或许保护一个内存块的安全方式是创建一个类,类包含了它自己身的 CRITICAL_SECTION ,以及构造、解析、访问方法。这种方法对保护对像来说,有着额外的好处,可以在 xlAutoOpen 运行之前进行初始化,以及在 xlAutoClose 后,还是有效的。但你要注意考虑创建太多的临界段时是否影响操作系统的性能。

当你的代码需要同时访问不止一个保护内存块时,你需要注意考虑内存的访问顺序。例如,下面的实例中的函数会创建一个死锁。

// WARNING: Do not copy this code. These two functions
// can produce a deadlock and are provided for
// example and illustration only.
bool copy_shared_table_element_A_to_B(unsigned int index)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTableA);
    EnterCriticalSection(&g_csSharedTableB);
    shared_table_B[index] = shared_table_A[index];
// Critical sections should be exited in the order
// they were entered, NOT as shown here in this
// deliberately wrong illustration.
    LeaveCriticalSection(&g_csSharedTableA);
    LeaveCriticalSection(&g_csSharedTableB);
    return true;
}

bool copy_shared_table_element_B_to_A(unsigned int index)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTableB);
    EnterCriticalSection(&g_csSharedTableA);
    shared_table_A[index] = shared_table_B[index];
    LeaveCriticalSection(&g_csSharedTableA);
    LeaveCriticalSection(&g_csSharedTableB);
    return true;
}

如果第一个线程上的函数进入g_csSharedTableA的同时,另一个纯种上的第二个函数,进入g_csSharedTableB, 那么这两个线程都会线程挂机。正确的方法是进入和退出的顺序保持一至。

EnterCriticalSection(&g_csSharedTableA);
    EnterCriticalSection(&g_csSharedTableB);
    // code that accesses both blocks
    LeaveCriticalSection(&g_csSharedTableB);
    LeaveCriticalSection(&g_csSharedTableA);

在可能的情况下,最好从一个线程可以合作的观点来隔离访问不同的块,如下所示。

bool copy_shared_table_element_A_to_B(unsigned int index)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTableA);
    double d = shared_table_A[index];
    LeaveCriticalSection(&g_csSharedTableA);
    EnterCriticalSection(&g_csSharedTableB);
    shared_table_B[index] = d;
    LeaveCriticalSection(&g_csSharedTableB);
    return true;
}