Multithreading- und Arbeitsspeicherkonflikte in Excel

Gilt für: Excel 2013 | Office 2013 | Visual Studio

Versionen von Microsoft Excel vor Excel 2007 verwenden einen einzelnen Thread für alle Arbeitsblattberechnungen. Ab Excel 2007 kann Excel jedoch für die Verwendung von 1 bis 1024 gleichzeitigen Threads für die Arbeitsblattberechnung konfiguriert werden. Auf einem Computer mit mehreren Prozessoren oder Mehreren Kernen entspricht die Standardanzahl von Threads der Anzahl der Prozessoren oder Kerne. Daher können threadsichere Zellen oder Zellen, die nur funktionen enthalten, die threadsicher sind, gleichzeitigen Threads zugewiesen werden, sofern die übliche Neuberechnungslogik gilt, die nach ihren Präzedenzfällen berechnet werden muss.

Thread-Safe-Funktionen

Die meisten integrierten Arbeitsblattfunktionen ab Excel 2007 sind threadsicher. Sie können auch XLL-Funktionen als threadsicher schreiben und registrieren. Excel verwendet einen Thread (Standard Thread), um alle Befehle, threadunsicheren Funktionen, xlAuto-Funktionen (außer xlAutoFree und xlAutoFree12) sowie COM- und Visual Basic for Applications (VBA)-Funktionen aufzurufen.

Wenn eine XLL-Funktion eine XLOPER - oder XLOPER12 mit xlbitDLLFree zurückgibt, verwendet Excel denselben Thread, in dem der Funktionsaufruf erfolgt ist, um xlAutoFree oder xlAutoFree12 aufzurufen. Der Aufruf von xlAutoFree oder xlAutoFree12 erfolgt vor dem nächsten Funktionsaufruf für diesen Thread.

Für XLL-Entwickler gibt es Vorteile beim Erstellen threadsicherer Funktionen:

  • Sie ermöglichen Es Excel, das Beste aus einem Multiprozessor- oder Multi-Core-Computer zu machen.

  • Sie eröffnen die Möglichkeit, Remoteserver viel effizienter zu verwenden, als dies mit einem einzelnen Thread möglich ist.

Angenommen, Sie verfügen über einen Einzelprozessorcomputer, der beispielsweise für die Verwendung von N Threads konfiguriert wurde. Angenommen, es wird eine Kalkulationstabelle ausgeführt, die eine große Anzahl von Aufrufen an eine XLL-Funktion ausführt, die wiederum eine Anforderung für Daten oder eine Berechnung an einen Remoteserver oder Servercluster sendet. Vorbehaltlich der Topologie der Abhängigkeitsstruktur könnte Excel die Funktion fast gleichzeitig N mal aufrufen. Vorausgesetzt, dass der Server oder die Server ausreichend schnell oder parallel sind, könnte die Neuberechnungszeit des Arbeitsblatts um den Faktor 1/N reduziert werden.

Das Hauptproblem beim Schreiben threadsicherer Funktionen ist die ordnungsgemäße Behandlung von Konflikten für Ressourcen. Dies bedeutet in der Regel Speicherkonflikte, die in zwei Probleme unterteilt werden können:

  • Erstellen von Arbeitsspeicher, von dem Sie wissen, dass er nur von diesem Thread verwendet wird.

  • Hier erfahren Sie, wie Sie sicherstellen, dass von mehreren Threads sicher auf gemeinsam genutzten Arbeitsspeicher zugegriffen wird.

Beachten Sie zunächst, auf welchen Arbeitsspeicher in einer XLL alle Threads zugreifen können und auf welche nur der aktuell ausgeführte Thread zugreifen kann.

Zugriff für alle Threads

  • Variablen, Strukturen und Klasseninstanzen, die außerhalb des Hauptteils einer Funktion deklariert werden.

  • Statische Variablen, die im Textkörper einer Funktion deklariert werden.

In diesen beiden Fällen wird arbeitsspeicher im Arbeitsspeicherblock der DLL, der für diesen instance der DLL erstellt wurde, reserviert. Wenn eine andere Anwendung die DLL instance lädt, erhält sie eine eigene Kopie dieses Arbeitsspeichers, sodass es keine Konflikte für diese Ressourcen von außerhalb dieses instance der DLL gibt.

Zugriff nur für den aktuellen Thread

  • Automatische Variablen im Funktionscode (einschließlich Funktionsargumenten).

In diesem Fall wird arbeitsspeicher für jeden instance des Funktionsaufrufs im Stapel reserviert.

Hinweis

Der Bereich des dynamisch zugeordneten Arbeitsspeichers hängt vom Bereich des Zeigers ab, der darauf verweist: Wenn alle Threads auf den Zeiger zugreifen können, ist der Speicher ebenfalls vorhanden. Wenn es sich bei dem Zeiger um eine automatische Variable in einer Funktion handelt, ist der zugeordnete Arbeitsspeicher für diesen Thread praktisch privat.

Arbeitsspeicher, auf den nur ein Thread zugreifen kann: Thread-Local Arbeitsspeicher

Da statische Variablen im Textkörper einer Funktion von allen Threads zugänglich sind, sind Funktionen, die sie verwenden, eindeutig nicht threadsicher. Eine instance der Funktion in einem Thread könnte den Wert ändern, während ein anderer instance in einem anderen Thread davon ausgeht, dass es sich um etwas völlig anderes handelt.

Es gibt zwei Gründe für das Deklarieren statischer Variablen innerhalb einer Funktion:

  1. Statische Daten bleiben von einem Aufruf zum nächsten erhalten.

  2. Ein Zeiger auf statische Daten kann von der Funktion sicher zurückgegeben werden.

Im ersten Fall möchten Sie möglicherweise Daten haben, die beibehalten werden und für alle Aufrufe der Funktion Bedeutung haben: vielleicht einen einfachen Zähler, der bei jedem Aufruf der Funktion in einem thread inkrementiert wird, oder eine Struktur, die Nutzungs- und Leistungsdaten bei jedem Aufruf sammelt. Die Frage ist, wie die freigegebenen Daten oder die Datenstruktur geschützt werden können. Dies geschieht am besten mit dem kritischen Abschnitt, wie im nächsten Abschnitt erläutert.

Wenn die Daten nur für die Verwendung durch diesen Thread vorgesehen sind, was aus Grund 1 der Fall sein könnte und immer aus Grund 2 der Fall ist, ist die Frage, wie Speicher erstellt werden kann, der beibehalten wird, aber nur über diesen Thread zugänglich ist. Eine Lösung besteht darin, die TLS-API (Thread-Local Storage) zu verwenden.

Betrachten Sie beispielsweise eine Funktion, die einen Zeiger auf eine XLOPER zurückgibt.

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;
}

Diese Funktion ist nicht threadsicher, da ein Thread die statische XLOPER12 zurückgeben kann, während er von einem anderen überschrieben wird. Die Wahrscheinlichkeit, dass dies geschieht, ist noch größer, wenn die XLOPER12 an xlAutoFree12 übergeben werden muss. Eine Lösung besteht darin, eine XLOPER12 zuzuweisen, einen Zeiger darauf zurückzugeben und xlAutoFree12 zu implementieren, damit der XLOPER12 Arbeitsspeicher selbst freigegeben wird. Dieser Ansatz wird in vielen der Beispielfunktionen verwendet, die in der Speicherverwaltung in Excel gezeigt werden.

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
}

Dieser Ansatz ist einfacher zu implementieren als der im nächsten Abschnitt beschriebene Ansatz, der auf der TLS-API basiert, hat aber einige Nachteile. Erstens muss Excel xlAutoFree xlAutoFree/ xlAutoFree12 unabhängig vom Typ des zurückgegebenen XLOPER-XLOPER12/ aufrufen. Zweitens gibt es ein Problem beim Zurückgeben von XLOPER-XLOPER12/ s, die den Rückgabewert eines Aufrufs einer C-API-Rückruffunktion darstellen. Die XLOPER-XLOPER12/ verweist möglicherweise auf Arbeitsspeicher, der von Excel freigegeben werden muss, aber dieXLOPER-XLOPER12/ selbst muss auf die gleiche Weise freigegeben werden, wie sie zugewiesen wurde. Wenn ein solcher XLOPER-XLOPER12/ als Rückgabewert einer XLL-Arbeitsblattfunktion verwendet werden soll, gibt es keine einfache Möglichkeit, xlAutoFree xlAutoFree/ xlAutoFree12 darüber zu informieren, dass beide Zeiger auf die richtige Weise freigegeben werden müssen. (Das Festlegen von xlbitXLFree und xlbitDLLFree löst das Problem nicht, da die Behandlung von XLOPER/XLOPER12s in Excel mit beiden Festgelegten nicht definiert ist und sich möglicherweise von Version zu Version ändern kann.) Um dieses Problem zu umgehen, kann die XLL tiefe Kopien aller excel-zugeordneten XLOPER/XLOPER12s erstellen, die an das Arbeitsblatt zurückgegeben werden.

Eine Lösung, die diese Einschränkungen vermeidet, ist das Auffüllen und Zurückgeben eines threadlokalen XLOPER/XLOPER12. Ein Ansatz, der erfordert, dass xlAutoFree/xlAutoFree12 den XLOPER/XLOPER12-Zeiger selbst nicht freigibt.

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!
}

Die nächste Frage ist, wie der threadlokale Arbeitsspeicher eingerichtet und abgerufen wird, d. h. wie die Funktion get_thread_local_xloper12 im vorherigen Beispiel implementiert wird. Dies erfolgt mithilfe der TLS-API (Thread Local Storage). Der erste Schritt besteht darin, mithilfe von TlsAlloc einen TLS-Index abzurufen, der letztendlich mit TlsFree freigegeben werden muss. Beides wird am besten über DllMain erreicht.

// 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;
}

Nachdem Sie den Index abgerufen haben, besteht der nächste Schritt darin, jedem Thread einen Speicherblock zuzuweisen. In der Windows-Entwicklungsdokumentation wird empfohlen, dies jedes Mal zu tun, wenn die DllMain-Rückruffunktion mit einem DLL_THREAD_ATTACH-Ereignis aufgerufen wird und der Arbeitsspeicher bei jedem DLL_THREAD_DETACH freigegeben wird. Wenn Sie diesen Ratschlag befolgen, führt ihre DLL jedoch zu unnötigen Aufgaben für Threads, die nicht für die Neuberechnung verwendet werden.

Stattdessen ist es besser, eine Zuweisungs-on-First-Use-Strategie zu verwenden. Zunächst müssen Sie eine Struktur definieren, die Sie für jeden Thread zuordnen möchten. Für die vorherigen Beispiele, die XLOPERs oder XLOPER12s zurückgeben, reicht Folgendes aus, aber Sie können eine beliebige Struktur erstellen, die Ihren Anforderungen entspricht.

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

Die folgende Funktion ruft einen Zeiger auf die threadlokale instance ab oder weist einen zu, wenn dies der erste Aufruf ist.

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;
}

Nun können Sie sehen, wie der threadlokale XLOPER/XLOPER12 Arbeitsspeicher abgerufen wird: Zuerst erhalten Sie einen Zeiger auf die instance der TLS_data des Threads und geben dann wie folgt einen Zeiger auf die darin enthaltene XLOPER/XLOPER12 zurück.

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;
}

Die Funktionen mtr_safe_example_1 und mtr_safe_example_2 können als threadsichere Arbeitsblattfunktionen registriert werden, wenn Sie Excel ausführen. Sie können die beiden Ansätze jedoch nicht in einer XLL kombinieren. Ihre XLL kann nur eine Implementierung von xlAutoFree und xlAutoFree12 exportieren, und jede Speicherstrategie erfordert einen anderen Ansatz. Bei mtr_safe_example_1 muss der an xlAutoFree/xlAutoFree12 übergebene Zeiger zusammen mit allen Daten, auf die er verweist, freigegeben werden. Mit mtr_safe_example_2 sollten nur die Spitzendaten freigegeben werden.

Windows bietet auch eine Funktion GetCurrentThreadId, die die eindeutige systemweite ID des aktuellen Threads zurückgibt. Dies bietet dem Entwickler eine weitere Möglichkeit, Codethreads zu schützen oder sein Verhalten threadspezifisch zu machen.

Speicherzugriff nur durch mehr als einen Thread: Kritische Abschnitte

Sie sollten den Lese-/Schreibspeicher schützen, auf den mehrere Threads zugreifen können, indem Sie kritische Abschnitte verwenden. Sie benötigen einen benannten kritischen Abschnitt für jeden Speicherblock, den Sie schützen möchten. Sie können diese während des Aufrufs der xlAutoOpen-Funktion initialisieren, sie freigeben und während des Aufrufs der xlAutoClose-Funktion auf NULL festlegen. Anschließend müssen Sie jeden Zugriff auf den geschützten Block innerhalb eines Paars von Aufrufen von EnterCriticalSection und LeaveCriticalSection enthalten. Es ist immer nur ein Thread in den kritischen Abschnitt zulässig. Hier sehen Sie ein Beispiel für die Initialisierung, Aufhebung der Initialisierung und Verwendung eines Abschnitts namens 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;
}

Eine weitere, vielleicht sicherere Möglichkeit zum Schutz eines Speicherblocks besteht darin, eine Klasse zu erstellen, die eine eigene CRITICAL_SECTION enthält und deren Konstruktor-, Destruktor- und Accessormethoden die Verwendung übernehmen. Dieser Ansatz hat den zusätzlichen Vorteil, dass Objekte geschützt werden, die vor der Ausführung von xlAutoOpen initialisiert werden oder nach dem Aufruf von xlAutoClose bestehen bleiben. Sie sollten jedoch vorsichtig sein, wenn Sie zu viele kritische Abschnitte erstellen und den dadurch verursachten Betriebssystemaufwand verwenden.

Wenn Sie über Code verfügen, der gleichzeitig Zugriff auf mehr als einen geschützten Speicherblock benötigt, müssen Sie die Reihenfolge, in der die kritischen Abschnitte betreten und beendet werden, sehr sorgfältig prüfen. Die folgenden beiden Funktionen können z. B. einen Deadlock erzeugen.

// 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;
}

Wenn die erste Funktion in einem Thread g_csSharedTableA während die zweite in einem anderen Thread in g_csSharedTableB wechselt, hängen beide Threads. Der richtige Ansatz besteht darin, in einer konsistenten Reihenfolge einzugeben und wie folgt in umgekehrter Reihenfolge zu beenden.

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

Wenn möglich, ist es aus Sicht der Thread-Zusammenarbeit besser, den Zugriff auf unterschiedliche Blöcke zu isolieren, wie hier gezeigt.

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;
}

Wenn es viele Konflikte mit einer freigegebenen Ressource gibt, z. B. häufige zugriffsbezogene Anforderungen mit kurzer Dauer, sollten Sie erwägen, die Fähigkeit des kritischen Abschnitts zum Drehen zu nutzen. Dies ist eine Technik, die das Warten auf die Ressource weniger prozessorintensiv macht. Dazu können Sie entweder InitializeCriticalSectionAndSpinCount beim Initialisieren des Abschnitts oder SetCriticalSectionSpinCount nach der Initialisierung verwenden, um festzulegen, wie oft die Threadschleifen ausgeführt werden, bevor auf die Verfügbarkeit von Ressourcen gewartet wird. Der Wartevorgang ist teuer, sodass das Drehen dies vermeidet, wenn die Ressource in der Zwischenzeit freigegeben wurde. Auf einem einzelnen Prozessorsystem wird die Drehzahl effektiv ignoriert, aber Sie können sie trotzdem angeben, ohne schaden zu müssen. Der Speicherheap-Manager verwendet eine Drehzahl von 4.000. Weitere Informationen zur Verwendung kritischer Abschnitte finden Sie in der Windows SDK-Dokumentation.

Siehe auch

Speicherverwaltung in Excel

Multithread-Neuberechnung in Excel

Add-In-Manager und XLL-Benutzeroberflächenfunktionen