Partager via


Traitement multithread et contention de mémoire dans Excel (traduction automatique)

Dernière modification : jeudi 6 mai 2010

S’applique à : Excel 2010 | Office 2010 | VBA | Visual Studio

Dans cet article
Fonctions de thread-Safe
Mémoire Accessible par un seul Thread : mémoire locale de Thread
Mémoire Accessible uniquement par plusieurs threads : les Sections critiques

Important

Cet article a été traduit automatiquement, voir l’avertissement. Vous pouvez consulter la version en anglais de cet article ici.

Les versions de Microsoft Excel antérieures à Excel 2007 utilisent un seul fil (thread) pour tous les calculs de feuille de calcul. Cependant, à partir d’Excel 2007, Excel peut être configuré de façon à utiliser 1 à 1024 threads simultanés pour les calculs de feuille de calcul. Sur un ordinateur multiprocesseur ou multicœur, le nombre par défaut de threads est égal au nombre de processeurs ou de cœurs. Par conséquent, les cellules sécurisées au niveau du thread, ou les cellules contenant uniquement des fonctions sécurisées au niveau du thread, peuvent être attribuées à des threads simultanés, soumis à la condition que la logique de recalcul habituelle soit calculée après ses antécédents.

Fonctions de thread-Safe

La plupart des fonctions intégrées de feuille de calcul commençant dans Excel 2007 sont thread-safe. Vous pouvez également écrire et enregistrer des fonctions XLL comme étant thread-safe. Excel utilise un seul thread (son thread principal) pour appeler toutes les commandes, fonctions non sécurisées de threads, fonctions xlAuto (sauf xlAutoFree et xlAutoFree12) et COM et Visual Basic pour Applications des fonctions (VBA).

Dans le cas où une fonction XLL renvoie une XLOPER ou XLOPER12 avec xlbitDLLFree défini, Excel utilise le même thread que celui sur lequel l'appel de fonction a été effectuée pour appeler xlAutoFree ou xlAutoFree12. L'appel à xlAutoFree ou xlAutoFree12 est effectué avant l'appel de fonction suivant sur ce thread.

Pour les développeurs XLL, il existe des avantages de création de fonctions de thread-safe :

  • Ils permettent à Microsoft Excel pour que le meilleur parti d'un ordinateur multiprocesseur ou multicœur.

  • Ils ouvrent la possibilité d'utiliser des serveurs distants beaucoup plus efficacement que ne peut être effectuée à l'aide d'un seul thread.

Supposons que vous disposiez d'un ordinateur à processeur unique qui a été configuré pour utiliser, par exemple, les threads de N . Supposons qu'une feuille de calcul est en cours d'exécution que fait un grand nombre d'appels à une fonction XLL qui à son tour envoie une demande de données ou pour un calcul à effectuer sur un serveur distant ou d'un cluster de serveurs. Soumis à la topologie de l'arborescence des dépendances, Excel peut appeler les heures de la fonction n presque simultanément. Sous réserve que l'ou les serveurs sont suffisamment rapide ou parallèle, la durée de recalcul de la feuille de calcul peut être réduite en autant comme facteur de 1/n.

La question clé dans l'écriture de fonctions de thread-safe gère correctement contention des ressources. Cela signifie en général la contention de mémoire, et il peut être divisé en deux problèmes :

  • La création de mémoire dont vous savez qu'il servira uniquement par ce thread.

  • Comment faire pour vous assurer que mémoire partagée est accessible par plusieurs threads en toute sécurité.

La première chose à savoir est quelle mémoire dans un XLL est accessible par tous les threads, et ce qui est accessible uniquement par le thread en cours d'exécution.

Accessible par tous les threads

  • Instances de classe, les structures et les variables déclarées en dehors du corps d'une fonction.

  • Variables statiques déclarées dans le corps d'une fonction.

Dans ces deux cas, mémoire est réservée dans le bloc de mémoire de la DLL créée pour cette instance de la DLL. Si une autre instance de l'application charge la DLL, il obtient sa propre copie de cette mémoire afin qu'il n'y a pas de conflit avec ces ressources en dehors de cette instance de la DLL.

Accessible uniquement par le thread en cours

  • Variables automatiques dans le code de fonction (y compris les arguments de la fonction).

Dans ce cas, mémoire définie à part sur la pile pour chaque instance de l'appel de fonction.

Notes

La portée de la mémoire allouée dynamiquement dépend de la portée du pointeur qui pointe vers lui : si le pointeur est accessible par tous les threads, la mémoire est également. Si le pointeur est une variable automatique dans une fonction, la mémoire allouée est effectivement privée pour ce thread.

Mémoire Accessible par un seul Thread : mémoire locale de Thread

Étant donné que les variables statiques dans le corps d'une fonction sont accessibles par tous les threads, les fonctions qui les utilisent ne sont clairement pas thread-safe. Une seule instance de la fonction sur un thread peut modifier la valeur pendant qu'une autre instance sur un autre thread est en supposant qu'il est quelque chose de complètement différent.

Il existe deux raisons pour déclarer des variables statiques au sein d'une fonction :

  1. Les données statiques sont persistantes d'un seul appel à l'autre.

  2. Un pointeur vers les données statiques en toute sécurité peut être renvoyé par la fonction.

Dans le cas de la première raison, vous pouvez souhaiter disposer de données qui est conservée et a une signification pour tous les appels à la fonction : par exemple un compteur simple qui est incrémenté chaque fois que la fonction est appelée sur n'importe quel thread, ou une structure qui collecte les données de performances et l'utilisation à chaque appel. La question est de savoir comment protéger les données partagées ou la structure de données. Cela mieux à l'aide de section critique comme l'explique la section suivante.

Si les données sont conçues uniquement pour utilisation par ce thread, ce qui pourrait être le cas pour raison 1 et est toujours le cas de raison 2, la question est la création de mémoire qui persiste, mais n'est accessible à partir de ce thread. Une solution consiste à utiliser le stockage local des threads (TLS) API.

Par exemple, considérons une fonction qui retourne un pointeur vers un 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;
}

Cette fonction n'est pas thread-safe, car un seul thread peut retourner la XLOPER12 statique tandis que l'autre est le remplacement. La probabilité de ce genre d'attaque est accrue si le XLOPER12 doit être transmis à xlAutoFree12. Une solution consiste à allouer un XLOPER12, renvoyer un pointeur vers elle et implémenter xlAutoFree12 afin que la mémoire XLOPER12 elle-même est libérée. Cette approche est utilisée dans la plupart des fonctions exemple indiquées dans Gestion de la mémoire dans Excel (traduction automatique).

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
}

Cette approche est plus simple à implémenter que l'approche décrite dans la section suivante, qui repose sur l'API TLS, mais elle comporte certains inconvénients. Tout d'abord, Excel doit appeler xlAutoFree/xlAutoFree12 quel que soit le type de la XLOPER/XLOPER12 retournée. Deuxièmement, il y a un problème lors du renvoi de XLOPER/XLOPER12s sont la valeur de retour d'un appel à une fonction de rappel API C. Le XLOPER/XLOPER12 peut pointer vers la mémoire doit être libérée par Excel, mais XLOPER/XLOPER12 lui-même doit être libérée de la même manière qu'il a été alloué. Si tel un XLOPER/XLOPER12 doit être utilisé comme valeur de retour d'une fonction de feuille de calcul XLL, il n'existe aucun moyen simple d'informer xlAutoFree/xlAutoFree12 de la nécessité de libérer les deux pointeurs de la manière appropriée. (Définition de la xlbitXLFree et le xlbitDLLFree ne résout pas le problème, comme le traitement de XLOPER/XLOPER12s dans Excel avec les deux ensemble n'est pas défini et peut-être changer à partir d'une version à l'autre.) Pour contourner ce problème, le XLL pouvez créer des copies approfondies de tous les XLOPER/XLOPER12s allouée à Excel qu'elle renvoie à la feuille de calcul.

Une solution pour éviter ces contraintes consiste à remplir et renvoyer une XLOPER/XLOPER12 locales de thread, une approche qui nécessite ce xlAutoFree/xlAutoFree12 ne libère pas le pointeur de la 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!
}

La question suivante consiste à configurer et récupérer la mémoire locale de thread, en d'autres termes, comment implémenter la fonction get_thread_local_xloper12 dans l'exemple précédent. Pour cela, à l'aide de la Thread Local Storage (TLS) API. La première étape consiste à obtenir un index TLS à l'aide de TlsAlloc, qui doit finalement être libérée en utilisant TlsFree. Les deux sont effectuées depuis 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;
}

Après avoir obtenu l'index, l'étape suivante consiste à allouer un bloc de mémoire pour chaque thread. La Documentation de développement Windows (éventuellement en anglais) recommande cette opération chaque fois que la fonction de rappel DllMain est appelée avec un événement de DLL_THREAD_ATTACH et libérer la mémoire sur chaque DLL_THREAD_DETACH. Toutefois, suivant ce Conseil provoquerait votre DLL faire le travail inutile pour les threads ne pas utilisé pour le nouveau calcul.

Au lieu de cela, il est préférable d'utiliser une stratégie d'allocation à la première utilisation. Tout d'abord, vous devez définir une structure que vous souhaitez allouer à chaque thread. Pour les exemples précédents qui renvoient des XLOPERs ou XLOPER12s, structure la suivante est suffisante, mais vous pouvez créer une structure qui répond à vos besoins.

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

La fonction suivante obtient un pointeur vers l'instance locale de thread, ou en alloue un s'il s'agit du premier appel.

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

Vous pouvez maintenant voir comment la mémoire locale de thread XLOPER/XLOPER12 est obtenue : tout d'abord, vous obtenez un pointeur vers l'instance du thread de TLS_data, et puis vous renvoyez un pointeur vers le XLOPER/XLOPER12 qu'il contient, comme suit.

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

Les fonctions mtr_safe_example_1 et mtr_safe_example_2 peuvent être inscrits en tant que fonctions de feuille de calcul thread-safe lorsque vous exécutez Excel. Toutefois, vous ne pouvez pas mélanger les deux approches dans un XLL. Votre XLL peut exporter qu'une seule implémentation de xlAutoFree et xlAutoFree12, et chaque stratégie de la mémoire nécessite une approche différente. Avec mtr_safe_example_1, le pointeur passé à xlAutoFree/xlAutoFree12 doit être libéré avec toutes les données qu'il pointe vers. Avec mtr_safe_example_2, uniquement les données vers lequel pointe doivent être libérées.

Windows fournit également une fonction GetCurrentThreadId, qui renvoie l'ID unique à l'échelle du système. du thread en cours Ainsi, le développeur avec une autre façon de sécuriser la thread de code, ou pour rendre son thread comportement spécifique.

Mémoire Accessible uniquement par plusieurs threads : les Sections critiques

Vous devez protéger la mémoire en lecture/écriture qui est accessible par plusieurs threads à l'aide de sections critiques. Vous avez besoin d'une section critique nommé pour chaque bloc de mémoire que vous souhaitez protéger. Vous pouvez initialiser pendant l'appel à la fonction xlAutoOpen et les libérer et affectez la valeur null lors de l'appel à la fonction xlAutoClose. Vous devrez alors contenir chaque accès au bloc protégé dans une paire d'appels à EnterCriticalSection et LeaveCriticalSection. Un seul thread est autorisé dans la section critique à tout moment. Voici un exemple de l'initialisation, de désinitialisation et utilisation d'une section appelée 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;
}

Un autre, peut-être plus sûr moyen de protection d'un bloc de mémoire consiste à créer une classe qui contient ses propres CRITICAL_SECTION et dont constructeur, destructeur et méthodes d'accesseurs prennent en charge de son utilisation. Cette approche présente l'avantage supplémentaire de protection des objets qui peuvent être initialisés avant de xlAutoOpen est exécuté ou survivre xlAutoClose est appelée, mais soyez prudent sur la création de trop de sections critiques et la surcharge de système d'exploitation que cela entraînerait la création.

Une fois que le code qui doit accéder à plusieurs blocs de mémoire protégés en même temps, vous devez tenir compte très soigneusement l'ordre dans lequel les sections critiques sont entrées et s'est terminées. Par exemple, les deux fonctions suivantes pourraient créer un blocage.

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

Si la première fonction sur un thread entre g_csSharedTableA alors que la seconde sur un autre thread passe g_csSharedTableB, les deux threads se bloquent. L'approche correcte consiste à entrer dans un ordre cohérent et sortir dans l'ordre inverse, comme suit.

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

Si possible, il est préférable d'un thread coopération point de vue d'isoler l'accès aux blocs distincts, comme illustré ici.

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

Dans le cas où il y a beaucoup de contention pour une ressource partagée, telles que des demandes d'accès fréquent de courte durée, vous devez envisager de spin à l'aide de la capacité de la section critique. Il s'agit d'une technique qui met en attente de la ressource moins intensifs. Pour ce faire, vous pouvez utiliser soit InitializeCriticalSectionAndSpinCount lors de l'initialisation de la section ou SetCriticalSectionSpinCount une fois initialisée, pour définir le nombre de fois que le thread effectue une boucle avant d'attendre les ressources soient disponibles. L'opération d'attente est coûteuse, et en rotation peut éviter cela, si la ressource est libérée entre-temps. Sur un système à processeur unique, le compteur de rotations est ignoré, mais vous pouvez malgré tout le préciser sans causer le moindre dommage. Le Gestionnaire de segments de mémoire utilise un compteur de rotations de 4000. Pour plus d'informations sur l'utilisation de sections critiques, consultez la documentation du SDK de Windows.

Notes

Avertissement traduction automatique : cet article a été traduit par un ordinateur, sans intervention humaine. Microsoft propose cette traduction automatique pour offrir aux personnes ne maîtrisant pas l’anglais l’accès au contenu relatif aux produits, services et technologies Microsoft. Comme cet article a été traduit automatiquement, il risque de contenir des erreurs de grammaire, de syntaxe ou de terminologie.

Voir aussi

Concepts

Gestion de la mémoire dans Excel (traduction automatique)

Recalcul multithread dans Excel (traduction automatique)

Gestionnaire de compléments et fonctions d’interface XLL (traduction automatique)