適用於:Excel 2013 |Office 2013 |Visualstudio
Excel 2007 之前的 Microsoft Excel 版本會針對所有工作表計算使用單個線程。 不過,從 Excel 2007 開始,Excel 可以設定為使用 1 到 1024 個並行線程來計算工作表。 在多處理器或多核心計算機上,默認線程數目等於處理器或核心數目。 因此,安全線程的儲存格,或只包含安全線程函式的儲存格,可以分配給並行線程,受限於需要在其前導之後計算的一般重新計算邏輯。
Thread-Safe 函式
從 Excel 2007 開始,大部分的內建工作表函式都是安全線程。 您也可以將 XLL 函式寫入並註冊為安全線程。 Excel 會使用一個線程 (其主線程) 來呼叫所有命令、線程不安全的函式、 xlAuto 函式 (除了 xlAutoFree 和 xlAutoFree12) ,以及 COM 和 Visual Basic for Applications (VBA) 函式。
當 XLL 函式傳回 XLOPER 或具有 xlbitDLLFree 集合的XLOPER12時,Excel 會使用呼叫 xlAutoFree 或 xlAutoFree12 的相同線程。 xlAutoFree 或 xlAutoFree12 的呼叫是在該線程上的下一個函數調用之前進行。
針對 XLL 開發人員,建立安全線程函式有其優點:
它們可讓 Excel 充分利用多處理器或多核心電腦。
相較於使用單一線程,它們可讓您更有效率地使用遠端伺服器。
假設您有一部已設定為使用 N 個線程的單一處理器電腦。 假設電子表格正在執行,它會對 XLL 函式進行大量呼叫,進而傳送數據要求,或對遠端伺服器或伺服器叢集執行計算。 根據相依性樹狀結構的拓撲,Excel 幾乎可以同時呼叫函式 N 次。 如果伺服器或伺服器的速度夠快或平行,電子表格的重新計算時間可以減少 1/N 的一倍。
撰寫安全線程函式的主要問題是正確處理資源爭用。 這通常表示記憶體爭用,而且可以分成兩個問題:
如何建立您知道只會由這個線程使用的記憶體。
如何確保多個線程安全地存取共用記憶體。
首先要注意的是,所有線程都可以存取 XLL 中的記憶體,以及目前執行中的線程才能存取的記憶體。
可供所有線程存取
在函式主體之外宣告的變數、結構和類別實例。
函式主體內宣告的靜態變數。
在這兩種情況下,會將記憶體在為此 DLL 實例建立的 DLL 記憶體區塊中。 如果另一個應用程式實例載入 DLL,它會取得它自己的記憶體復本,因此這些資源不會從這個 DLL 實例外部競爭。
只能由目前的線程存取
- 函式程式代碼內的自動變數 (包括函式自變數) 。
在此情況下,記憶體會保留在函數調用每個實例的堆疊上。
注意事項
動態設定的記憶體範圍取決於指向它的指標範圍:如果所有線程都可以存取指標,記憶體也會是 。 如果指標是函式中的自動變數,則配置的記憶體實際上是該線程的私用。
只有一個線程可存取的記憶體:Thread-Local 記憶體
假設所有線程都可以存取函式主體內的靜態變數,使用這些變數的函式顯然並不安全線程。 一個線程上的函式實例可能會變更值,而另一個線程上的另一個實例則假設其完全不同。
在函式中宣告靜態變數有兩個原因:
靜態數據會從一次呼叫持續到下一個呼叫。
函式可以安全地傳回靜態數據的指標。
在第一個原因的情況下,您可能想要有保存的數據,而且對函式的所有呼叫都具有意義:可能是每次在任何線程上呼叫函式時遞增的簡單計數器,或是在每次呼叫時收集使用量和效能數據的結構。 問題在於如何保護共享數據或數據結構。 最好使用關鍵區段來完成,如下一節所述。
如果數據僅供此線程使用,這可能是原因 1 的原因,且一律是原因 2 的案例,問題在於如何建立保存但只能從此線程存取的記憶體。 其中一個解決方案是使用線程本機記憶體 (TLS) API。
例如,請考慮傳回 XLOPER 指標的函式。
LPXLOPER12 WINAPI mtr_unsafe_example(LPXLOPER12 pxArg)
{
static XLOPER12 xRetVal; // memory shared by all threads!!!
// code sets xRetVal to a function of pxArg ...
return &xRetVal;
}
此函式不是安全線程,因為一個線程可以傳回靜態 XLOPER12 ,而另一個線程正在覆寫它。 如果需要將 XLOPER12 傳遞至 xlAutoFree12,發生這種情況的可能性仍然較大。 其中一個解決方案是配置 XLOPER12、傳回指標並實作 xlAutoFree12 ,以便釋放 XLOPER12 記憶體本身。 此方法用於 Excel 的記憶體管理中所示的許多範例函式。
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,但有一些缺點。 首先,Excel 必須呼叫 xlAutoFree/ xlAutoFree12 任何傳回 之 XLOPER/ XLOPER12的類型。 其次,傳回 XLOPER/ XLOPER12是 C API 回調函式呼叫的傳回值時發生問題。 XLOPER/ XLOPER12可能會指向需要由 Excel 釋放的記憶體,但 XLOPER/ XLOPER12本身必須以相同的配置方式釋放。 如果要使用這類 XLOPER/ XLOPER12 做為 XLL 工作表函式的傳回值,則沒有簡單的方法可以通知 xlAutoFree/ xlAutoFree12 是否需要以適當方式釋放這兩個指標。 (設定 xlbitXLFree 和 xlbitDLLFree 並無法解決問題,因為這兩個集合在 Excel 中處理 XLOPER/XLOPER12 的 處理方式未定義,而且可能會從版本變更為 version.) 若要解決此問題,XLL 可以建立所有 Excel 配置 的 XLOPER/XLOPER12 的深層複本,以將其傳回工作表。
避免這些限制的解決方案是填入並傳回線程本機 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 在上一個範例中。 這是使用線程本機記憶體 (TLS) API 來完成。 第一個步驟是使用 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 開發文件建議每次使用DLL_THREAD_ATTACH事件呼叫 DllMain 回調函式時執行此動作,並在每個DLL_THREAD_DETACH釋放記憶體。 不過,遵循此建議會導致您的 DLL 針對未用於重新計算的線程執行不必要的工作。
相反地,最好使用配置優先使用策略。 首先,您必須定義要為每個線程配置的結構。 針對先前傳回 XLOPER 或 XLOPER12s 的範例,下列已足夠,但您可以建立符合您需求的任何結構。
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;
}
當您執行 Excel 時,mtr_safe_example_1和mtr_safe_example_2函式可以註冊為安全線程的工作表函式。 不過,您無法在一個 XLL 中混合這兩種方法。 您的 XLL 只能匯出 一個 xlAutoFree 和 xlAutoFree12 實作,而且每個記憶體策略都需要不同的方法。 使用 mtr_safe_example_1,傳遞至 xlAutoFree/xlAutoFree12 的指標必須與其指向的任何數據一起釋放。 使用 mtr_safe_example_2,應該只釋放指向的數據。
Windows 也提供 GetCurrentThreadId 函式,此函式會傳回目前線程的唯一全系統識別符。 這可為開發人員提供另一種方式來確保程式代碼線程安全,或使其行為線程成為特定。
只有一個以上的線程可存取記憶體:重要區段
您應該使用重要區段來保護多個線程可以存取的讀取/寫入記憶體。 您想要保護的每個記憶體區塊都需要具名關鍵區段。 您可以在呼叫 xlAutoOpen 函式期間初始化這些專案,並釋放它們,並在呼叫 xlAutoClose 函式期間將它們設定為 null。 接著,您必須在 一對 EnterCriticalSection 和 LeaveCriticalSection 呼叫內,包含受保護區塊的每個存取權。 任何時候都只允許一個線程進入關鍵區段。 以下是初始化、取消初始化和使用名為 g_csSharedTable 區段的範 例。
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;
}
當共用資源發生許多爭用,例如頻繁的短期存取要求時,您應該考慮使用關鍵區段的微調功能。 這是一種技術,可讓等候資源的處理器較少。 若要這樣做,您可以在初始化區段時使用 InitializeCriticalSectionAndSpinCount 或初始化 SetCriticalSectionSpinCount ,以在等候資源變成可用之前設定線程循環的次數。 等候作業的成本很高,因此,如果同時釋放資源,則旋轉可避免這種情況。 在單一處理器系統上,會有效地忽略微調計數,但您仍然可以指定它,而不會造成任何損害。 記憶體堆積管理員使用 4000 的微調計數。 如需使用重要區段的詳細資訊,請參閱 Windows SDK 檔。