Frage

Ich habe eine benutzerdefinierte Thread -Pool -Klasse, in der einige Threads erstellt werden, die jede auf ihrem eigenen Ereignis warten (Signal). Wenn ein neuer Job zum Thread -Pool hinzugefügt wird, weckt er den ersten kostenlosen Thread, damit er den Job ausführt.

Das Problem ist das folgende: Ich habe rund 1000 Schleifen von jeweils um 10'000 Iterationen. Diese Schleifen müssen nacheinander ausgeführt werden, aber ich habe 4 CPUs verfügbar. Ich versuche zu tun, die 10'000 Iterationsschleifen in 4 2'500 Iterations -Schleifen zu teilen, dh eine pro Thread. Aber ich muss warten, bis die 4 kleinen Loops fertig sind, bevor ich zur nächsten "großen" Iteration gehe. Dies bedeutet, dass ich die Jobs nicht bündeln kann.

Mein Problem ist, dass die Verwendung des Thread -Pools und 4 Threads viel langsamer ist als die Aufgaben nacheinander zu erledigen (eine Schleife durch einen separaten Thread ist viel langsamer, als es direkt im Haupt -Thread nacheinander auszuführen).

Ich bin unter Windows, also erstelle ich Ereignisse mit CreateEvent() Und dann warten Sie auf einen von ihnen, der benutzt WaitForMultipleObjects(2, handles, false, INFINITE) bis der Hauptfaden ruft SetEvent().

Es scheint, dass dieses ganze Ereignissache (zusammen mit der Synchronisation zwischen den Threads mit kritischen Abschnitten) ziemlich teuer ist!

Meine Frage ist: Ist es normal, dass die Verwendung von Ereignissen viel Zeit braucht? Wenn ja, gibt es einen weiteren Mechanismus, den ich verwenden könnte, und das wäre weniger zeitgleicher?

Hier ist ein Code zum Veranschaulichung (einige relevante Teile, die aus meiner Thread -Pool -Klasse kopiert werden):

// 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));
}
War es hilfreich?

Lösung

Wenn Sie nur Loops parallelisieren und VS 2008 verwenden, würde ich vorschlagen, OpenMP zu betrachten. Wenn Sie Visual Studio 2010 Beta 1 verwenden, würde ich vorschlagen, das anzusehen Parallele Musterbibliothek, besonders die "Parallele für" / "Parallele für jede" APIs oder der "Aufgabengruppe Klasse, weil diese wahrscheinlich das tun, was Sie versuchen, nur mit weniger Code.

In Bezug auf Ihre Frage zur Leistung kommt es hier wirklich an. Sie müssen sich ansehen, wie viel Arbeit Sie während Ihrer Iterationen planen und welche Kosten sind. WaitFormultiPipleObjects können ziemlich teuer sein, wenn Sie es viel treffen und Ihre Arbeit klein ist. Deshalb empfehle ich, eine bereits erstellte Implementierung zu verwenden. Sie müssen auch sicherstellen, dass Sie unter einem Debugger nicht im Debug -Modus laufen und dass die Aufgaben selbst nicht auf einem Sperre, I/O oder Speicherallokation blockieren und Sie keine falsche Freigabe treffen. Jedes von diesen hat das Potenzial, Skalierbarkeit zu zerstören.

Ich würde vorschlagen, dies unter einem Profiler zu betrachten, wie Xperf Der F1 -Profiler in Visual Studio 2010 Beta 1 (es verfügt über 2 neue Parallelitätsmodi, die sehen, dass die Konkurrenz ansieht) oder Intels Vtune.

Sie können auch den Code, den Sie in den Aufgaben ausführen profilierte es. "

Viel Glück

-Rick

Andere Tipps

Dies sieht für mich als Produzent -Verbrauchermuster aus, das mit zwei Semaphoren in Verbindung gebracht werden kann, die den Warteschlangenüberlauf bewachen, die andere die leere Warteschlange.

Sie können einige Details finden hier.

Ja, WaitForMultipleObjects ist ziemlich teuer. Wenn Ihre Jobs klein sind, wird der Synchronisierungsaufwand die Kosten für den Job, wie Sie sehen, überfordern.

Eine Möglichkeit, dies zu beheben, ist das Bündel mehrerer Jobs in einen: Wenn Sie einen "kleinen" Job erhalten (wie auch immer Sie solche Dinge bewerten), speichern Sie ihn an einem Ort, bis Sie genügend kleine Jobs zusammen haben, um einen angemessenen Job zu machen. Senden Sie dann alle zur Verarbeitung an einen Arbeiter -Thread.

Alternativ können Sie anstatt die Signalübertragung zu verwenden, um Ihre Jobs mit mehreren Leiter zu verwenden, um Ihre Jobs zu speichern. In diesem Modell versucht jeder Arbeiter -Thread, Jobs aus der Warteschlange zu nehmen. Wenn es einen findet, erledigt es den Job; Wenn dies nicht der Fall ist, schläft es für kurze Zeit, wacht dann auf und versucht erneut. Dies senkt Ihren Overhead pro Aufgabe, aber Ihre Threads werden CPU annehmen, selbst wenn keine Arbeit zu erledigen ist. Es hängt alles von der genauen Art des Problems ab.

Achten Sie auf, Sie fragen immer noch um einen nächsten Job, nachdem der Enderssignal ausgestrahlt wurde.

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

Es sollte nicht so teuer sein, aber wenn Ihr Job überhaupt kaum Zeit dauert, wird der Overhead der Threads und Synchronisierungsobjekte erheblich. Thread-Pools wie diese eignen sich viel besser für längere Verarbeitungsjobs oder für diejenigen, die viel IO anstelle von CPU-Ressourcen verwenden. Wenn Sie bei der Verarbeitung eines Jobs CPU-gebunden sind, stellen Sie sicher, dass Sie nur einen Thread pro CPU haben.

Es kann andere Probleme geben. Wie kann GetNextJob seine Daten verarbeiten lassen? Wenn eine große Menge an Daten kopiert, haben Sie Ihren Aufwand wieder erheblich erhöht.

Ich würde es optimieren, indem ich jeden Thread weiterhin Jobs von der Warteschlange ziehe, bis die Warteschlange leer ist. Auf diese Weise können Sie hundert Jobs an den Thread -Pool weitergeben und die Synchronisierungsobjekte werden nur als einmal verwendet, um den Thread zu starten. Ich würde die Jobs auch in einer Warteschlange speichern und einen Zeiger, einen Referenz oder einen Iterator an sie an den Thread weitergeben, anstatt die Daten zu kopieren.

Der Kontextwechsel zwischen Threads kann ebenfalls teuer sein. In einigen Fällen ist es interessant, ein Framework zu entwickeln, mit dem Sie Ihre Jobs nacheinander mit einem Thread oder mit mehreren Threads verarbeiten können. Auf diese Weise können Sie das Beste aus den beiden Welten haben.

Was ist übrigens genau Ihre Frage? Ich werde in der Lage sein, genauer mit einer genaueren Frage zu beantworten :)

BEARBEITEN:

Der Ereignisteil kann in einigen Fällen mehr als Ihre Verarbeitung konsumieren, sollte jedoch nicht so teuer sein, es sei denn, Ihre Verarbeitung ist sehr schnell zu erreichen. In diesem Fall ist das Umschalten zwischen Thredas ebenfalls teuer, daher ist mein Antwort der erste Teil, wenn es darum geht, die Dinge sequenziell zu machen ...

Sie sollten nach Threads-Synchronisation Engpässen suchen. Sie können die Wartezeiten verfolgen, um mit ...

Bearbeiten: Nach weiteren Hinweisen ...

Wenn ich es richtig denke, besteht Ihr Problem darin, alle Ihre Computerkerne/Prozessoren effizient zu verwenden, um einige Verarbeitungs -Essenscialy -Sequenziale zu verarbeiten.

Nehmen Sie, dass Sie 4 Kerne und 10000 Schleifen haben, um wie in Ihrem Beispiel zu berechnen (in einem Kommentar). Sie sagten, dass Sie warten müssen, bis die 4 Threads enden, bevor Sie fortfahren. Dann können Sie Ihren Synchronisationsprozess vereinfachen. Sie müssen nur Ihre vier Threads Thr. Sie sollten ein Rendezvous oder eine Barriere verwenden (einen Synchronisationsmechanismus, der auf die Abschluss von N -Threads wartet). Schub hat einen solchen Mechanismus. Sie können die Windows -Implementierung nach Effizienz suchen. Ihr Thread -Pool ist nicht wirklich für die Aufgabe geeignet. Die Suche nach einem verfügbaren Thread in einem kritischen Abschnitt tötet Ihre CPU -Zeit. Nicht der Ereignisteil.

Es scheint, dass dieses ganze Ereignissache (zusammen mit der Synchronisation zwischen den Threads mit kritischen Abschnitten) ziemlich teuer ist!

"Teur" ist ein relativer Begriff. Sind Jets teuer? Sind Autos? oder Fahrräder ... Schuhe ...?

In diesem Fall lautet die Frage: Sind Ereignisse "teur" im Verhältnis zu der Zeit, die für die Ausführung von Jobfunktionen benötigt wird? Es würde helfen, einige absolute Zahlen zu veröffentlichen: Wie lange dauert der Prozess, wenn "nicht überbeeitet" ist? Ist es Monate oder ein paar Femtosekunden?

Was passiert mit der Zeit, wenn Sie die Threadpool -Größe erhöhen? Versuchen Sie eine Poolgröße von 1, dann 2, dann 4 usw.

Da Sie hier in der Vergangenheit einige Probleme mit Threadpools hatten, würde ich ein Debug vorschlagen, um zu zählen, wie oft Ihre Threadfunktion tatsächlich aufgerufen wird ... Passt es zu dem, was Sie erwarten?

Wenn Sie eine Figur aus der Luft auswählen (ohne etwas über Ihr Zielsystem zu wissen, und vorausgesetzt, Sie tun nichts "Großes" in Code, das Sie nicht gezeigt haben), würde ich das "Ereignisaufwand" eines jeden "Jobs" erwarten, wenn Sie nicht gezeigt haben). in Mikrosekunden gemessen werden. Vielleicht hundert oder so. Wenn die Zeit, die für die Durchführung des Algorithmus in der Jobfunktion benötigt wird, nicht wesentlich mehr als dieses Mal ist, dürften Ihre Threads Sie Zeit kosten, anstatt ihn zu sparen.

Da sagst du, dass es ist viel Ich gehe parallel als sequentielle Ausführung langsamer, und ich gehe davon aus, dass Ihre Verarbeitungszeit für Ihre internen 2500 -Schleifen -Iterationen winzig ist (im Bereich der wenigen Mikrosekunden). Dann gibt es nicht viel, was Sie tun können, außer Ihren Algorithmus zu überprüfen, um größere Voraussetzungen zu teilen. OpenMP hilft nicht und alle anderen Synchronisierungstechniken werden auch nicht helfen, da sie grundsätzlich alle auf Ereignisse angewiesen sind (Spinschleifen qualifizieren sich nicht).

Wenn Ihre Verarbeitungszeit der 2500 -Schleifen -Iterationen hingegen größer als 100 Mikrosekunden (auf aktuellen PCs) beträgt, werden Sie möglicherweise Einschränkungen der Hardware eingehen. Wenn Ihre Verarbeitung eine Menge Speicherbandbreite verwendet, gibt es Ihnen aufgrund von Kollisionen tatsächlich weniger Bandbreite, sie auf vier Prozessoren aufzuteilen. Sie könnten auch auf Probleme des Cache -Radfahrens stoßen, bei denen jede Ihrer Top 1000 -Iteration den Cache der 4 Kerne spüle und neu laden. Dann gibt es keine Lösung, und abhängig von Ihrer Zielhardware kann es keine geben.

Wie bereits erwähnt, hängt die Menge an Overheads, die durch Threading hinzugefügt wurde, von der relativen Zeit ab, die für die von Ihnen definierten "Jobs" benötigt wird. Daher ist es wichtig, ein Gleichgewicht in der Größe der Arbeitsbrocken zu finden, die die Anzahl der Teile minimiert, aber die Prozessoren nicht im Leerlauf warten, bis die letzte Gruppe von Berechnungen abgeschlossen ist.

Ihr Codierungsansatz hat die Menge an Gemeinkosten erhöht, indem Sie aktiv nach einem Leerlauffaden sucht, um neue Arbeiten zu liefern. Das Betriebssystem hält dies bereits im Auge und macht es viel effizienter. Außerdem kann Ihr Funktion Threadpool :: addjob () feststellen, dass alle Threads verwendet werden und die Arbeit nicht delegieren können. Es bietet jedoch keinen Rückgaberocode, der sich auf dieses Problem bezieht. Wenn Sie in irgendeiner Weise nicht auf diesen Zustand prüfen und keine Fehler in den Ergebnissen bemerken, bedeutet dies, dass es immer Leerlaufprozessoren gibt. Ich würde vorschlagen, den Code neu zu organisieren, damit Addjob () das tut, was er genannt wird - nur einen Job hinzufügt (ohne zu finden oder sogar zu kümmern, wer den Job macht), während jeder Arbeiter -Thread aktiv neue Arbeiten bekommt, wenn er mit seiner vorhandenen Arbeit erledigt ist.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top