Question

J'ai une classe de pool de thread personnalisée, qui crée des threads qui attendent chacun sur leur propre événement (signal). Lorsqu'un nouveau travail est ajouté au pool de threads, il réveille le premier thread gratuit afin qu'il exécute le travail.

Le problème est le suivant: j'ai environ 1000 boucles de chacune environ 10 000 itérations. Ces boucles doivent être exécutées séquentiellement, mais j'ai 4 processeurs disponibles. Ce que j'essaie de faire, c'est de diviser les boucles d'itération de 10'000 en boucles d'itérations de 4 2'500, c'est-à-dire une par fil. Mais je dois attendre que les 4 petites boucles se terminent avant d'aller à la prochaine "grande" itération. Cela signifie que je ne peux pas regrouper les travaux.

Mon problème est que l'utilisation du pool de threads et 4 threads est beaucoup plus lente que de faire les travaux séquentiellement (avoir une boucle exécutée par un thread séparé est beaucoup plus lente que de l'exécuter directement dans le thread principal séquentiellement).

Je suis sous Windows, donc je crée des événements avec CreateEvent() Et puis attendez l'un d'eux en utilisant WaitForMultipleObjects(2, handles, false, INFINITE) Jusqu'à ce que le thread principal appelle SetEvent().

Il semble que tout ce truc d'événement (ainsi que la synchronisation entre les threads à l'aide de sections critiques) est assez cher!

Ma question est: est-il normal que l'utilisation d'événements prenne "beaucoup de temps"? Si oui, y a-t-il un autre mécanisme que je pourrais utiliser et qui serait moins cher?

Voici un code à illustrer (certaines pièces pertinentes copiées à partir de ma classe de pool de threads):

// thread function
unsigned __stdcall ThreadPool::threadFunction(void* params) {
    // some housekeeping
    HANDLE signals[2];
    signals[0] = waitSignal;
    signals[1] = endSignal;

    do {
        // wait for one of the signals
        waitResult = WaitForMultipleObjects(2, signals, false, INFINITE);

        // try to get the next job parameters;
        if (tp->getNextJob(threadId, data)) {
            // execute job
            void* output = jobFunc(data.params);

            // tell thread pool that we're done and collect output
            tp->collectOutput(data.ID, output);
        }

        tp->threadDone(threadId);
    }
    while (waitResult - WAIT_OBJECT_0 == 0);

    // if we reach this point, endSignal was sent, so we are done !

    return 0;
}

// create all threads
for (int i = 0; i < nbThreads; ++i) {
    threadData data;
    unsigned int threadId = 0;
    char eventName[20];

    sprintf_s(eventName, 20, "WaitSignal_%d", i);

    data.handle = (HANDLE) _beginthreadex(NULL, 0, ThreadPool::threadFunction,
        this, CREATE_SUSPENDED, &threadId);
    data.threadId = threadId;
    data.busy = false;
    data.waitSignal = CreateEvent(NULL, true, false, eventName);

    this->threads[threadId] = data;

    // start thread
    ResumeThread(data.handle);
}

// add job
void ThreadPool::addJob(int jobId, void* params) {
    // housekeeping
    EnterCriticalSection(&(this->mutex));

    // first, insert parameters in the list
    this->jobs.push_back(job);

    // then, find the first free thread and wake it
    for (it = this->threads.begin(); it != this->threads.end(); ++it) {
        thread = (threadData) it->second;

        if (!thread.busy) {
            this->threads[thread.threadId].busy = true;

            ++(this->nbActiveThreads);

            // wake thread such that it gets the next params and runs them
            SetEvent(thread.waitSignal);
            break;
        }
    }

    LeaveCriticalSection(&(this->mutex));
}
Était-ce utile?

La solution

Si vous parallilez des boucles et utilisez VS 2008, je vous suggère de regarder OpenMP. Si vous utilisez Visual Studio 2010 Beta 1, je suggérerais de regarder le bibliothèque de motifs parallèles, en particulier le "parallèle pour" / "parallèle pour chaque" API ou la "Groupe de travail classe parce que ceux-ci feront probablement ce que vous essayez de faire, seulement avec moins de code.

En ce qui concerne votre question sur les performances, cela dépend vraiment. Vous devrez regarder la quantité de travail que vous planifiez pendant vos itérations et quels sont les coûts. WaitFormultipleObjects peut être assez cher si vous le frappez beaucoup et que votre travail est petit, c'est pourquoi je suggère d'utiliser une implémentation déjà construite. Vous devez également vous assurer que vous ne fonctionz pas en mode débogage, sous un débogueur et que les tâches elles-mêmes ne bloquent pas sur une serrure, une allocation d'E / S ou de mémoire, et que vous n'atteignez pas de faux partage. Chacun d'eux a le potentiel de détruire l'évolutivité.

Je vous suggère de regarder cela sous un profileur comme xperf Le F1 Profiler dans Visual Studio 2010 Beta 1 (il a 2 nouveaux modes de concurrence qui aident à voir les affirmations) ou Intel's Vtune.

Vous pouvez également partager le code que vous exécutez dans les tâches, afin que les gens puissent avoir une meilleure idée de ce que vous faites, car la réponse que j'obtiens toujours avec les problèmes de performances est d'abord "cela dépend" et deuxième " le profilé. "

Bonne chance

-Meule

Autres conseils

Cela me ressemble à un modèle de consommateur producteur, qui peut être imparti avec deux sémaphores, l'un gardant le débordement de la file d'attente, l'autre la file d'attente vide.

Vous pouvez trouver des détails ici.

Oui, WaitForMultipleObjects est assez cher. Si vos emplois sont petits, les frais généraux de synchronisation commencent à submerger le coût de la réellement du travail, comme vous le voyez.

Une façon de résoudre ce problème consiste à intégrer plusieurs travaux en un seul: si vous obtenez un "petit" emploi (mais vous évaluez de telles choses), stockez-le quelque part jusqu'à ce que vous ayez suffisamment de petits travaux pour faire un travail de taille raisonnable. Ensuite, envoyez-les tous à un fil de travail pour le traitement.

Alternativement, au lieu d'utiliser la signalisation, vous pouvez utiliser une file d'attente à rédacteur unique à lecture multiple pour stocker vos travaux. Dans ce modèle, chaque fil de travailleur essaie de saisir des travaux de la file d'attente. Quand il en trouve un, il fait le travail; Si ce n'est pas le cas, il dort pendant une courte période, puis se réveille et essaie à nouveau. Cela réduira vos frais généraux par tâche, mais vos fils prendront le processeur même lorsqu'il n'y a pas de travail à faire. Tout dépend de la nature exacte du problème.

Attention, vous demandez toujours un prochain emploi après l'émission de la fin de la fin.

for( ;; ) {
    // wait for one of the signals
    waitResult = WaitForMultipleObjects(2, signals, false, INFINITE);
    if( waitResult - WAIT_OBJECT_0 != 0 )
        return;
    //....
}

Cela ne devrait pas être si cher, mais si votre travail ne prend pratiquement aucun temps, alors les frais généraux des threads et des objets de synchronisation deviendront significatifs. Les pools de threads comme celui-ci fonctionnent beaucoup mieux pour les travaux de transformation plus longs ou pour ceux qui utilisent beaucoup d'IO au lieu des ressources CPU. Si vous êtes lié au processeur lors du traitement d'un travail, assurez-vous que vous n'avez qu'un seul thread par CPU.

Il peut y avoir d'autres problèmes, comment GetNextJob a-t-il le traitement de ses données? S'il y a une grande quantité de copie de données, vous avez à nouveau augmenté vos frais généraux.

Je l'optimiserais en laissant chaque fil continuer à retirer les travaux de la file d'attente jusqu'à ce que la file d'attente soit vide. De cette façon, vous pouvez transmettre une centaine de travaux au pool de threads et les objets Sync seront utilisés juste une fois pour lancer le fil. Je stockerais également les travaux dans une file d'attente et je leur transmet un pointeur, une référence ou un itérateur au fil au lieu de copier les données.

Le changement de contexte entre les threads peut également être coûteux. Il est intéressant dans certains cas de développer un cadre que vous pouvez utiliser pour traiter vos travaux séquentiellement avec un seul thread ou avec plusieurs threads. De cette façon, vous pouvez avoir le meilleur des deux mondes.

Au fait, quelle est votre question exactement? Je pourrai répondre plus précisément avec une question plus précise :)

ÉDITER:

La partie d'événements peut consommer plus que votre traitement dans certains cas, mais ne devrait pas être aussi coûteuse, à moins que votre traitement ne soit très rapide à réaliser. Dans ce cas, basculer entre les thredas coûte également cher, d'où ma première partie de la première partie de faire les choses séquentialement ...

Vous devriez rechercher des goulots d'étranglement de synchronisation inter-threads. Vous pouvez tracer des temps d'attente pour commencer ...

EDIT: Après plus d'indices ...

Si je suppose correctement, votre problème est d'utiliser efficacement tous les cœurs / processeurs de vos ordinateurs pour parraller un traitement Essenaly séquentiel.

Prenez que vous avez 4 cœurs et 10000 boucles à calculer comme dans votre exemple (dans un commentaire). Vous avez dit que vous devez attendre que les 4 fils se terminent avant de continuer. Ensuite, vous pouvez simplifier votre processus de synchronisation. Il vous suffit de donner vos quatre threads au cours du nth + 1, nth + 2, nth + 3 boucles, attendez que les quatre threads se terminent puis se poursuivent. Vous devez utiliser un rendez-vous ou une barrière (un mécanisme de synchronisation qui attend que les threads n se terminent). Augmenter a un tel mécanisme. Vous pouvez rechercher la mise en œuvre de Windows pour l'efficacité. Votre pool de fil n'est pas vraiment adapté à la tâche. La recherche d'un fil disponible dans une section critique est ce qui tue votre temps de processeur. Pas la partie de l'événement.

Il semble que tout ce truc d'événement (ainsi que la synchronisation entre les threads à l'aide de sections critiques) est assez cher!

"Cher" est un terme relatif. Les jets sont-ils chers? Sont des voitures? Ou des vélos ... chaussures ...?

Dans ce cas, la question est: les événements sont-ils «coûteux» par rapport au temps pris pour que l'emploi de s'exécuter? Cela aiderait à publier des chiffres absolus: combien de temps dure le processus lorsqu'il est "non fidèle"? Est-ce des mois, ou quelques Femtosecondes?

Qu'arrive-t-il à l'heure à mesure que vous augmentez la taille du threadpool? Essayez une taille de piscine de 1, puis 2 puis 4, etc.

De plus, comme vous avez eu des problèmes avec Threadpools ici dans le passé, je suggérerais un débogage pour compter le nombre de fois où votre threadfunction est réellement invoqué ... Cela correspond-il à ce que vous attendez?

Choisir une figure hors de l'air (sans rien savoir de votre système cible, et en supposant que vous ne faites rien de "énorme" dans le code que vous n'avez pas montré), je m'attendais à ce que "l'événement aérien" de chaque "travail" à mesurer en microsecondes. Peut-être une centaine. Si le temps pris pour effectuer l'algorithme dans Jobfunction n'est pas beaucoup plus que cette fois, vos fils sont susceptibles de vous coûter du temps plutôt que de le sauver.

Puisque tu dis que c'est beaucoup Plus lent en parallèle que l'exécution séquentielle, je suppose que votre temps de traitement pour vos itérations de boucle interne 2500 est minuscule (dans la gamme de quelques micro secondes). Ensuite, il n'y a pas grand-chose que vous puissiez faire, sauf passer en revue votre algorithme pour diviser des morceaux de précession plus gros; OpenMP ne vous aidera pas et toutes les autres techniques de synchronisation ne vous aideront pas non plus car elles comptent fondamentalement sur les événements (les boucles de spin ne sont pas admissibles).

D'un autre côté, si votre temps de traitement des itérations de la boucle 2500 est supérieur à 100 micro secondes (sur les PC actuels), vous pourriez avoir des limites du matériel. Si votre traitement utilise beaucoup de bande passante de mémoire, le diviser en quatre processeurs ne vous donnera pas plus de bande passante, il vous donnera en fait moins à cause des collisions. Vous pourriez également rencontrer des problèmes de cyclisme du cache où chacune de vos 1000 premiers itération rincera et rechargera le cache des 4 cœurs. Ensuite, il n'y a pas de solution unique, et selon votre matériel cible, il peut y en avoir.

Comme mentionné précédemment, le montant des frais généraux ajoutés par le filetage dépend du temps relatif pris pour effectuer les "travaux" que vous avez définis. Il est donc important de trouver un équilibre dans la taille des morceaux de travail qui minimise le nombre de pièces mais ne laisse pas les processeurs inactifs en attente du dernier groupe de calculs.

Votre approche de codage a augmenté la quantité de travaux aériens en recherchant activement un fil inactif à fournir de nouveaux travaux. Le système d'exploitation suit déjà cela et le fait beaucoup plus efficacement. De plus, votre fonction Threadpool :: addJob () peut constater que tous les threads sont utilisés et ne peuvent pas déléguer le travail. Mais il ne fournit aucun code de retour lié à ce problème. Si vous ne vérifiez pas cette condition d'une manière ou d'une autre et que vous ne remarquez pas des erreurs dans les résultats, cela signifie qu'il y a toujours des processeurs inactifs. Je suggère de réorganiser le code pour qu'AddJob () fasse son nom - ajoute un emploi uniquement (sans trouver ni même se soucier de qui fait le travail) tandis que chaque fil de travail obtient activement un nouveau travail lorsqu'il est terminé avec son travail existant.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top