Pthread_yield mit "Zielthread"?



  • Hallo an alle!

    Da das Thema threads/pthreads nicht wirklich direkt nur C/C++ betrifft, fand ich dieses Forum am passendsten. Falls ein Moderator einen anderen Platz besser finde, bitte verschieben 😉

    Meine Frage bezieht sich auf das freigeben von CPU-Zeit mittels pthread_yield:

    Da ich beim Schreiben meines Verwendungszwecks bemerkt habe, dass dieser vielleicht nicht jeden interessiert und ich sehr weit ausgeholt habe, stelle ich die eigentliche Frage mal voran:

    Gibt es eine Funktion, ähnlich pthread_yield(), welche die CPU-Zeit eines Threads nicht nur wahllos freigibt, sondern sie einem definierbaren anderen Thread zuteilt? Dies kann sehr sinnvoll sein, wenn dem Programmierer bekannt ist, welcher Thread mit hoher Wahrscheinlichkeit ungeblockt arbeit verrichten kann.

    In meinem Spezialfall ist das nämlich so:

    Zum sammeln von Erfahrungen schreibe ich an unterschiedlichen Implementierungen zur synchronisation von Threads im folgenden Sinne:

    Es existieren (beispielsweise) 10 verschiedene Threads, welche irgendeine Schleife "Anzahl[Threadnummer]" oft durchführen sollen, um im Anschluss daran darauf zu warten, dass die jeweils anderen 9 Threads mit der Ausführung ihrer Schleifen fertig sind.

    Ich habe eine Implementierung mit Conditional Variables gebastelt, eine mit Mutexen, eine mit dem (eigentlich dafür vorgesehenen) Befehl pthread_barrier_wait.

    Interessanterweise ist die schnellste Lösung diejenige, die mit Mutexen (oder mit Spinlocks) arbeitet.

    Da das locken von Mutexen relativ viel Zeit kostet, versucht ein weiterer Ansatz, mithilfe einer Art verketter Liste ohne das Locken von Mutexen auszukommen.

    Stattdessen verwende ich sigwait und pthread_kill folgendermaßen:

    Ein Thread ist fertig mit seinen Loops. Daher greift er auf die Integervariable "ready" seines Vorgängers lesend zu. Ist dieser fertig, setzt er sein eigenes ready-flag ebenfalls auf true, sendet SIGCONT an seinen Nachfolgerthread und legt sich selbst schlafen.

    Der Nachfolgethread seinerseits ist entweder selbst schon fertig und schläft, erhält also das Signal, um aufzuwachen, oder er registriert nach Abarbeiten seines Loops, dass sein Vorgängerthread schon fertig ist.

    Liest ein Thread, dass sein Vorgängerthread NICHT fertig ist, so legt er sich zunächst schlafen, OHNE sein eigenes ready-flag auf 1 zu setzen. Auf diese Weise ist sichergestellt, dass, wenn das letzte Glied der Liste beim Vorgänger ready == 1 liest, tatsächlich ALLE Teilnehmer fertig sind.

    In diesem Fall befinden sich also alle anderen Teilnehmer im schlafenden Zustand. Der letzte Teilnehmer signalisiert dann dem Ersten, dass er aufwachen und seinen Loop fortsetzen darf, welcher dies ebenfalls mit seinem Nachfolger tut, etc.

    Das Ganze funktioniert wundervoll, der Algorithmus lief viele Minuten stabil und alle Threads hatten danach exakt gleich viele Durchläufe hinter sich (bis auf natürlich eine Differenz von maximal einem Gesamtdurchlauf) und das Ganze kam Performancetechnisch bis auf 10% an die Mutex-Implementierung heran.

    Um Deadlocks zu vermeiden, wird die Signalisierung des nächsten Threads mithilfe von pthread_kill allerdings in einem Loop durchlaufen, der auf das Umschalten eines Statusflags durch den Nachfolgethread wartet.

    Ich muss wohl nicht erwähnen, dass es an der Stelle Sinn macht, mit usleep(1) die CPU freizugeben und auf das Aufwachen des Nachfolgethreads zu warten.

    Ohnedem ist die Performance nicht einmal halb so gut. Mich stellt diese Lösung aber nicht wirklich zufrieden: Möglicherweise geht das Ganze ja auch viel schneller... Man könnte natürlich auch nanosleep nehmen, aber letztlich wäre das ja bloß Rumprobiererei.

    Ich habe es auch mit pthread_yield versucht... leider weniger effektiv als usleep.

    Lange Rede - Ich hoffe, dass sie nicht zu lang war - kurzer Sinn:
    Am schönsten wäre es doch, wenn ein Thread an einen Anderen mit thread_kill nicht nur ein Signal zum Wecken schicken könnte, sondern diesem im Anschluss daran auch seine CPU-Zeit überlassen und sich selbst schlafen legen könnte.

    Gibt es eine solche Funktion möglicherweise?

    Beste Grüße



  • V0lle85 schrieb:

    Da ich beim Schreiben meines Verwendungszwecks bemerkt habe, dass dieser vielleicht nicht jeden interessiert und ich sehr weit ausgeholt habe, stelle ich die eigentliche Frage mal voran:

    Es sei dir gedankt 🙂

    Gibt es eine Funktion, ähnlich pthread_yield(), welche die CPU-Zeit eines Threads nicht nur wahllos freigibt, sondern sie einem definierbaren anderen Thread zuteilt?

    Ich kenne keine, bin aber kein PTHREADs Experte. Mit Windows Threads kenne ich mich dafür sehr gut aus, und auch da kenne ich keine solche Funktion. (Yielden tut man da ja üblicherweise mit Sleep(0), und das war's dann auch schon)

    Dies kann sehr sinnvoll sein, wenn dem Programmierer bekannt ist, welcher Thread mit hoher Wahrscheinlichkeit ungeblockt arbeit verrichten kann.

    Ein Thread kann im Prinzip nur 3 Zustände haben:
    * Running
    * Runnable ("Ready")
    * Waiting

    Yield() gibt natürlich immer nur Rechenzeit an Threads ab, die "Runnable" sind. "Running" Threads laufen ja schon, und "waiting" Threads sind bekanntermassen blockiert, also können die auch keine Rechenzeit erhalten. Von daher sehe ich da mal grundsätzlich kein Problem.

    In meinem Spezialfall ist das nämlich so:
    (...)

    Könntest du mal die Variante mit Mutexen und die mit Condition-Variablen posten?
    (Mit Condition-Variablen ist mir halbwegs klar, aber wie man das sinnvoll mit Mutexen machen kann check ich grad nicht)

    Und noch zwei Fragen: auf wie vielen Cores testest du das ganze? Und testest du mit exakt gleichen Workloads für die 10 Threads, oder auch zusätzlich mit unterschiedlichen Workloads, so dass die Threads erzwungenermassen unterschiedlich lange brauchen?

    Und guck dir auf jeden Fall die Implementierung in Boost.Thread an. Also würde ich zumindest machen, rein schon aus Interesse wie die das machen, und die schnell deren Variante ist. Falls sie einfach die PTHREADs Funktion aufrufen guck in die Windows Implementierung rein, da gibt's keine OS/PTHREADs Funktion die sie nehmen können.



  • Vielen Dank für die schnelle Antwort!

    Auch auf die Gefahr hin, dass mir erfahrene Programmierer den Kopf einschlagen, werde ich meine Varianten hier mal posten. Das Ganze ist mehr "quick and dirty" zum Testen, da ich nicht viel Zeit habe. Die "Workloads" sind bewusst unterschiedlich gewählt. Es geht mir um Emulation von Prozessoren unterschiedlicher Geschwindigkeit, wie ich sie multithreaded ob meines beschränkten Wissensstandes lösen würde.

    Bevor ich den Code poste noch eine Frage:
    Die Zustände "Waiting, runnable und running" sind eine interessante Info. Allerdings stelle ich mir die Frage: Wenn Thread 1 ein Signal zum Aufwachen an Thread 2 schickt, ist dieser dann "direkt" wieder runnable, und daher mit yield direkt wieder verfügbar? Mir scheint das irgendwie nicht der Fall zu sein, denn die usleep-Version meines Programms ist offenbar schneller als die yield-Version.

    Und hier noch eine kleine Korrektur: Wenn ich die Häufigkeit der Synchronisation hochdrehe, indem ich bei allen Threads die Anzahl der Cycles verkleinere, wird die Listen-Version irgendwann doch beträchtlich langsamer (nur noch ca 30% der Mutex-Variante).

    So, hier endlich die Codes...

    Condition Variablen:

    #include <pthread.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <stdint.h>
    
    #define NTHREADS 10
    #define DURATION 10
    
    pthread_mutex_t comm1;
    pthread_cond_t cond1;
    
    pthread_t t [ NTHREADS ];
    
    uint64_t c [ NTHREADS ];
    
    uint8_t waiting;
    
    struct args
    {
    	pthread_mutex_t *mtx;
    	pthread_cond_t *cdv;
    	uint64_t *c;
    	uint8_t * waiting;
    	uint8_t yourNum;
    	uint64_t yourSyncCyc;	
    };
    
    void * countNext(void * argVoid)
    {
    	pthread_mutex_t dummy;
    	pthread_mutex_init(&dummy, NULL),
    	pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
    	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    	struct args arg = *((struct args*)argVoid);
    	while (1)
    	{
    		pthread_mutex_lock(arg.mtx);
    		*(arg.waiting)=*(arg.waiting)+1;
    		if (*(arg.waiting) < NTHREADS)
    		{
    			pthread_cond_wait(arg.cdv,arg.mtx);
    			pthread_mutex_unlock(arg.mtx);
    		}
    		else
    		{
    			*(arg.waiting) = 0;
    			pthread_mutex_unlock(arg.mtx);
    			pthread_cond_broadcast(arg.cdv);
    		}
    
    		uint64_t i = 0;
    		for (i = 0; i < arg.yourSyncCyc; i++)
    		{
    //			if (arg.c[arg.yourNum] % 100000 == 0)
    //				printf("%u: %lu, %u\n",arg.yourNum, arg.c[arg.yourNum],arg.yourSyncCyc);
    			(arg.c[arg.yourNum])++;
    		}
    	}
    }
    
    int main(void)
    {
    	uint64_t yourSyncCyc [ NTHREADS ];
    	yourSyncCyc[0] = 300;
    	yourSyncCyc[1] = 150;
    	yourSyncCyc[2] = 150;
    	yourSyncCyc[3] = 64;
    	yourSyncCyc[4] = 64;
    	yourSyncCyc[5] = 64;
    	yourSyncCyc[6] = 64;
    	yourSyncCyc[7] = 64;
    	yourSyncCyc[8] = 64;
    	yourSyncCyc[9] = 64;
    
    	pthread_mutex_init(&comm1, NULL);
    	pthread_cond_init(&cond1, NULL);
    	pthread_mutex_lock(&comm1);
    
    	struct args arg[NTHREADS];
    
    	int i;
    	for (i=0; i<NTHREADS; i++)
    	{
    		arg[i].mtx = &comm1;
    		arg[i].cdv =  &cond1;
    		arg[i].c = c;
    		arg[i].waiting = &waiting;
    		arg[i].yourNum = i;
    		arg[i].yourSyncCyc = yourSyncCyc[i];
    		printf("Starting thread %u.\n",arg[i].yourNum);
    		pthread_create(&t[arg[i].yourNum], NULL, &countNext,&arg[i]);
    	}
    
    	pthread_mutex_unlock(&comm1);
    	sleep(DURATION);
    	for (i=0; i<NTHREADS; i++)
    		pthread_cancel(t[i]);
    	usleep(10000);
    	for (i=0; i<NTHREADS; i++)
    		printf("%lu\n",c[i]);
    
    	return 0;
    }
    

    Mutexe:

    #include <pthread.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <stdint.h>
    
    #define NTHREADS 10
    #define DURATION 600
    
    #ifdef USE_SPINLOCK
    	pthread_spinlock_t comm [NTHREADS];
    	pthread_spinlock_t waitingLock;
    #else
    	pthread_mutex_t comm [NTHREADS];
    	pthread_mutex_t waitingLock;
    #endif
    
    pthread_t t [NTHREADS];
    uint8_t waiting = 0;
    volatile uint64_t c [NTHREADS];
    
    uint32_t synCyc[NTHREADS];
    
    struct args
    {
    #ifdef USE_SPINLOCK
    	pthread_spinlock_t *locks;
    	pthread_spinlock_t *waitingLock;
    #else
    	pthread_mutex_t *locks;
    	pthread_mutex_t *waitingLock;
    #endif
    volatile	uint64_t *c;
    volatile	uint8_t * waiting;
    	uint8_t totalThreads;
    	uint8_t yourNum;
    	uint32_t yourSynCyc;
    };
    
    void * countNext(void * argVoid)
    {
    	pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    	struct args arg = *((struct args*)argVoid);
    	printf("synCyc is %u\n",arg.yourSynCyc);
    	while (1)
    	{
    //		printf("%u waiting for lock\n",arg.yourNum);
    #ifdef USE_SPINLOCK
    		pthread_spin_lock(&arg.locks[arg.yourNum]);
    #else
    		pthread_mutex_lock(&arg.locks[arg.yourNum]);
    #endif
    //		printf("%u Unlocked\n",arg.yourNum);
    		int i;
    		for (i=0;i<arg.yourSynCyc;i++)
    			arg.c[arg.yourNum]=arg.c[arg.yourNum]+1;
    
    //		if (*(arg->c) % 100000 == 0)
    //
    
    #ifdef USE_SPINLOCK
    		pthread_spin_lock(arg.waitingLock);
    		*(arg.waiting) = *(arg.waiting)+1;
    		if (*(arg.waiting) == arg.totalThreads)
    		{
    			int i;
    			for (i=0; i<arg.totalThreads; i++)
    				pthread_spin_unlock(&arg.locks[i]);
    			*(arg.waiting) = 0;
    		}
    		pthread_spin_unlock(arg.waitingLock);
    #else
    		pthread_mutex_lock(arg.waitingLock);
    		*(arg.waiting) = *(arg.waiting)+1;
    		if (*(arg.waiting) == arg.totalThreads)
    		{
    			int i;
    			for (i=0; i<NTHREADS; i++)
    				pthread_mutex_unlock(&arg.locks[i]);
    			*(arg.waiting) = 0;
    		}
    		pthread_mutex_unlock(arg.waitingLock);
    #endif
    	}
    }
    
    int main(void)
    {
    	synCyc[0] = 300;
    	synCyc[1] = 150;
    	synCyc[2] = 150;
    	synCyc[3] = 64;
    	synCyc[4] = 64;
    	synCyc[5] = 64;
    	synCyc[6] = 64;
    	synCyc[7] = 64;
    	synCyc[8] = 64;
    	synCyc[9] = 64;
    
    #ifdef USE_SPINLOCK
    
    	int i = 0;
    	for (i=0; i< NTHREADS; i++)
    	{
    		pthread_spin_lock(&comm[i]);
    		pthread_spin_init(&comm[i], PTHREAD_PROCESS_PRIVATE);
    	}
    	pthread_spin_init(&waitingLock, PTHREAD_PROCESS_PRIVATE);
    #else
    	int i = 0;
    	for (i=0; i< NTHREADS; i++)
    	{
    		pthread_mutex_init(&comm[i], NULL);
    		pthread_mutex_lock(&comm[i]);
    	}
    	pthread_mutex_init(&waitingLock,NULL);
    #endif
    	struct args arg;
    	arg.locks = comm;
    	arg.c = c;
    	arg.totalThreads = NTHREADS;
    	arg.waiting = &waiting;
    	arg.waitingLock = &waitingLock;
    	for (arg.yourNum = 0; arg.yourNum < NTHREADS; arg.yourNum++)
    	{
    		printf("Starting thread %u\n",arg.yourNum);
    		arg.yourSynCyc = synCyc[arg.yourNum];
    		pthread_create(&t[arg.yourNum], NULL, &countNext,&arg);
    		usleep(10000);
    	}
    	for (i=0; i< NTHREADS; i++)
    	{
    #ifdef USE_SPINLOCK
    		pthread_spin_unlock(&comm[i]);
    #else
    		pthread_mutex_unlock(&comm[i]);
    #endif
    	}
    	sleep(DURATION);
    	for (i=0; i< NTHREADS; i++)
    	{	
    //		printf("%u\n",i);
    		pthread_cancel(t[i]);
    	}
    	for (i=0; i< NTHREADS; i++)
    		printf("%lu\n",c[i]);
    	return 0;
    }
    

    Ich hoffe, dass Du durch den Code durchsteigst, ich weiß, dass er alles Andere als schön ist. Auch die sleep-Befehle die dafür sorgen, dass die Threads genug Zeit zum Übernehmen der Übergabeparameter haben sind eher historisch gewachsen, weil ich Schreibarbeit sparen wollte..

    Dennoch: Nimm das Ganze ruhig auseinander, evtl erfahre ich ja auch etwas, das ich nicht wusste! 😉

    [edit:] Nur um Konfusion zu vermeiden: Bei mir meckert der Compiler nicht, wenn ich NTHREADS auf 5 stelle und trotzdem synSyc 10 Werte zuweise. Ich denke, dass man das trotzdem entsprechend auskommentieren sollte 😉

    Außerdem: Die counter-Variablen müssen rein technisch zwar nicht volatile sein, um aber zu verhindern, dass der Compiler die Schleife wegoptimiert, sind sie es dennoch.

    Gruß,
    V0lle85



  • Die Mutexen-Variante ist falsch: du darfst eine Mutex nur in dem Thread unlocken, der sie auch gelockt hat. Du unlockst aber Mutexen die ganz andere Threads gelockt haben.
    Erlaubt wäre es mit einer "binary semaphore". Diese wäre, im Vergleich zur Mutex "unowned", d.h. jeder beliebige Thread kann increment/decrement (lock/unlock) machen.

    Mag sein dass das System auf dem du testest es dir durchgehen lässt, aber laut Standard ist es soweit ich weiss nicht erlaubt.
    (BTW: hat jmd. nen Link zum "offiziellen" PTHREADs Standard bzw. der "offiziellen" API Doku?)

    ----

    Die Spinlock-Variante müsste OK sein. Ich kann in der Doku nix darüber finden, ob nur derjenige Thread unlock() machen darf, der auch lock() gemacht hat. Müsste also erlaubt sein. Allerdings ist die Variante ziemlich hardcore, weil sie massiv Rechenzeit verschwendet.

    ----

    Die Condition-Variable Variante ist auch strenggenommen falsch, da du "spurious wakeups" nicht behandelst. Ein "spurious wakeup" ist, wenn pthread_cond_wait() zurückkehrt, ohne dass irgendwer signal() oder broadcast() gemacht hat.
    Soweit ich weiss gibt es keinen vernünftigen Grund, warum das jemals passieren sollte (ja, ich kenne das David R. Butenhof-Zitat, aber ich meine mich zu erinnern von anderen threading-Profis gelesen zu haben, dass das Unsinn ist den einfach jeder immer wieder abschreibt). Allerdings sagt die API dass es passieren darf, also "muss" man als Programmierer damit rechnen, und seinen Code entsprechend darauf auslegen.

    ----

    Allgemein: du machst da Dinge, die deine Threads unnötig ausbremsen werden, und die Messung verfälschen.

    z.B. liegen deine Counter ("c") direkt hintereinander im Speicher. Bei 8 Byte pro Counter und sagen wir mal 64 Byte pro Cacheline, werden also 8 Counter in der selben Cache-Line liegen.
    Wenn jetzt zwei Threads gleichzeitig auf zwei Cores laufen, die die Counter in der selben Cache-Line verwenden, dann nehmen sich die zwei Cores andauernd gegenseitig die Cache-Line weg.
    Das kann (und wird IMO) dazu führen, dass es im Endeffekt deutlich langsamer läuft, als wenn die beiden Threads hintereinander laufen würden.
    Dann fällt nämlich der ganze Cache-Synchronisierungs-Overhead weg, und jeder Thread kann ungestört mit voller Geschwindigkeit zählen.

    Und das könnte dazu führen, dass Varianten die sich irgendwo unnötig ausbremsen, bei deinen Tests besser abschneiden, weil weniger Threads gleichzeitig laufen.

    An anderen Stellen wird u.U. das selbe passieren. Wenn du Benchmarks für sowas schreibst, musst du im Prinzip überall auf solche Dinge achten, damit du nicht Dinge misst, die du gar nicht messen wolltest.

    Und du verwendest auch nur drei verschiedene "Workload-Grössen" (300, 150 und 64). Ich würde da mehr variieren. Und ich würde auch empfehlen Tests mit unterschiedlicher Workload-Verteilung zu testen. Also z.B. eine Verteilung wo alle Threads eine gleich grosse "Aufgabe" bekommen, eine so wie du sie jetzt hast, und noch mehrere andere, bis hin zu total unterschiedlichen Grössen. Auch mal eine wo ein Thread 10x mehr als alle anderen zu tun bekommt, oder wo manche Threads so-gut-wie gar nix machen.



  • Hey!
    Danke für die wirklich hilfreiche Antwort 😉

    Die Mutexen-Variante ist falsch: du darfst eine Mutex nur in dem Thread unlocken, der sie auch gelockt hat. Du unlockst aber Mutexen die ganz andere Threads gelockt haben.
    Erlaubt wäre es mit einer "binary semaphore". Diese wäre, im Vergleich zur Mutex "unowned", d.h. jeder beliebige Thread kann increment/decrement (lock/unlock) machen.

    Sowas passiert, wenn man learning-by-doing veranstaltet. In der Tat benutze ich Mutexe in mehreren kleinen Projekten zur Synchronisation, da Condition Variablen mir zu dem Zeitpunkt nicht bekannt waren... Bisher läuft alles Problemlos. Es handelt sich dabei um eine Serverapplikation, die kein Problem mit weit über 1000 Anfragen pro Sekunde hat. Das macht es zwar nicht "richtiger", erklärt aber vllt meine fiese Verwendung von Mutexen.

    Die Spinlock-Variante müsste OK sein. Ich kann in der Doku nix darüber finden, ob nur derjenige Thread unlock() machen darf, der auch lock() gemacht hat. Müsste also erlaubt sein. Allerdings ist die Variante ziemlich hardcore, weil sie massiv Rechenzeit verschwendet.

    Du meinst, weil ein Spinlock den Kern nicht freigibt?
    Das dachte ich mir auch, im Falle sehr kleiner Werte von SynCyc wird aber wohl ein ContextSwitch oft sogar teurer sein. (?)

    Die Condition-Variable Variante ist auch strenggenommen falsch, da du "spurious wakeups" nicht behandelst. Ein "spurious wakeup" ist, wenn pthread_cond_wait() zurückkehrt, ohne dass irgendwer signal() oder broadcast() gemacht hat.

    Da muss ich dir leider mal widersprechen:
    Beim spurious wakeup handelt es sich meines Wissens um ein gleichzeitiges Aufwachen zweier Threads beim ausführen von cond_signal, welches eigentlich ja nur einen thread wecken soll (habe ich zumindest gestern irgendwo so gelesen). Da ich aber ohnehin broadcaste und die wartenden Threads auch im Anschluss garnicht auf irgendeine Variable zugreifen, die geschützt werden müssten, ist das hier kein Problem.

    Allgemein: du machst da Dinge, die deine Threads unnötig ausbremsen werden, und die Messung verfälschen.

    Da hast du wiederum Recht. Ob du's glaubst oder nicht, das ist mir beim Posten auch bewusst geworden. Werde ich auf jeden Fall nochmal testen.

    Was die falsche PThread-Variante angeht, wäre ich übrigens auch sehr über eine Doku erfreut. Insbesondere würde ich gern wissen, ob die "Falschheit" auf technischen Gründen beruht, oder auf der schweren Les- und Wartbarkeit des Codes.

    Danke für die ausführliche Antwort!

    Gruß,
    V0lle85



  • V0lle85 schrieb:

    Die Spinlock-Variante müsste OK sein. Ich kann in der Doku nix darüber finden, ob nur derjenige Thread unlock() machen darf, der auch lock() gemacht hat. Müsste also erlaubt sein. Allerdings ist die Variante ziemlich hardcore, weil sie massiv Rechenzeit verschwendet.

    Du meinst, weil ein Spinlock den Kern nicht freigibt?
    Das dachte ich mir auch, im Falle sehr kleiner Werte von SynCyc wird aber wohl ein ContextSwitch oft sogar teurer sein. (?)

    Schon richtig, ja.
    Bloss hat deine Maschine 10 Cores?
    Wenn du N Worker hast, die alle (bis auf den letzten) spinnen bis die Barrier "freigegeben" wird, und M Cores, dann muss mindestens N-(M+1) mal ein Thread bis zum Ende seiner Zeitscheibe spinnen. Wenn ich mich nicht vertan habe.
    Und so eine Zeitscheibe ist mächtig lange. Und bei auf Serverbetrieb getunten Betriebssystemen gleich nochmal länger (hab was von 40-50ms in Erinnerung bei Windows NT Server - weiss aber nicht ob das bei 2003/2008 Server auch noch so lange ist).

    Die Condition-Variable Variante ist auch strenggenommen falsch, da du "spurious wakeups" nicht behandelst. Ein "spurious wakeup" ist, wenn pthread_cond_wait() zurückkehrt, ohne dass irgendwer signal() oder broadcast() gemacht hat.

    Da muss ich dir leider mal widersprechen:
    Beim spurious wakeup handelt es sich meines Wissens um ein gleichzeitiges Aufwachen zweier Threads beim ausführen von cond_signal, welches eigentlich ja nur einen thread wecken soll (habe ich zumindest gestern irgendwo so gelesen). Da ich aber ohnehin broadcaste und die wartenden Threads auch im Anschluss garnicht auf irgendeine Variable zugreifen, die geschützt werden müssten, ist das hier kein Problem.

    Uff. Ich kann in keiner PTHREADs Doku die ich online auf die schnelle finde was definitives finden.
    Bei Wikipedia
    http://en.wikipedia.org/wiki/Spurious_wakeup
    steht eindeutig

    One of these reasons is a spurious wakeup; that is, a thread might get woken up even though no thread signalled the condition.

    Also meine Auslegung.
    Aber wie gesagt: ich bin der Meinung, dass die "spurious wakeups" eine Programmierer-Urban-Legend sind.

    Was die falsche PThread-Variante angeht, wäre ich übrigens auch sehr über eine Doku erfreut. Insbesondere würde ich gern wissen, ob die "Falschheit" auf technischen Gründen beruht, oder auf der schweren Les- und Wartbarkeit des Codes.

    Was meinst du jetzt?

    Die Geschichte mit den Mutexen und freigeben aus nem anderen Thread? Googel einfach nach pthread_mutex_unlock, da findest du genug PTHREADs API Dokus. Und in jeder guten die du findest, sollte stehen, dass das nicht erlaubt ist.

    Und was technische Gründe ja oder nein angeht... öhm.

    Die Forderung dass nur der Thread der lock() gemacht hat auch unlock() machen darf, ermöglicht vermutlich ein paar Optimierungen. Vor wenn man a) rekursive Mutexen haben möchte und b) kein Error-Checking bzw. kein definiertes Verhalten bei Fehlverwendung verlangt. (Wobei es solche Mutexen AFAIK bei PTHREADs nicht gibt, soweit ich weiss impliziert da "rekursiv" auch "error checking")

    Das ist aber IMO nicht der Grund. Der Grund ist einfach, dass man es so haben möchte, weil es mächtig viel Sinn macht.



  • Heho!

    Zur Vollständigkeit nochmal zum Thema 'spurious wakeups':
    http://pubs.opengroup.org/onlinepubs/009604499/functions/pthread_cond_signal.html

    Dort habe ich's gelesen, unter "rationale". Mit Codebeispiel, das ich mir aber nicht genau angeschaut habe..

    Und ja, ich meinte das aufrufen von unlock aus einem Thread, der den Lock nicht geholt hat.

    Klar kann man sagen, dass man es so haben möchte, weil es viel Sinn macht... Wenn es aber keine technischen Gründe gibt, die ein undefiniertes Verhalten induzieren, halte ich es für falsch, den Programmierer einzuschränken.

    Aber das ist wohl eine Grundsatzfrage. 😉

    Beste Grüße noch einmal!

    V0lle85



  • V0lle85 schrieb:

    Klar kann man sagen, dass man es so haben möchte, weil es viel Sinn macht... Wenn es aber keine technischen Gründe gibt, die ein undefiniertes Verhalten induzieren, halte ich es für falsch, den Programmierer einzuschränken.

    Naja...

    Eine Mutex ist halt nunmal so definiert, dass sie "owned" ist, also dem Thread "gehört" der sie gelockt hat. Das hat auch viele Vorteile.

    Man könnte natürlich was anderes implementieren, bloss dann sollte man es nicht mehr Mutex nennen, sondern "binary semaphore".

    PTHREADs wurde auch nicht mit dem Ziel entworfen alle möglichen low-level Optimierungen zu ermöglichen, sondern mit dem Ziel, dass man es möglichst einfach korrekt anwenden kann. Zumindest hab' ich das mal wo gelesen (weiss aber nimmer wo), und es scheint mir auch passend wenn ich mir die API angucke.

    Mit Mutexen und Condition-Variablen kann man auch recht einfach eine Barrier implementieren, ist also nicht so als ob etwas fehlen würde.

    Es gibt aber natürlich noch andere APIs, die andere Dinge anbieten. Unter Windows hat man Events, Semaphoren und Atomic-Operations. Alle drei würden sich ebenfalls eignen eine Barrier zu basteln. Mit Events oder Semaphoren wäre der Spass vermutlich relativ langsam. Mit Atomic-Operations (+ Events, damit man "spin-sleep" wait statt reinem "spin" wait machen kann) könnte man allerdings sicher was relativ flottes auf die Beine stellen - wenn auch nicht ganz einfach.

    Der neue C++ Standard bietet - neben klassischen Mutexen + Condition-Variablen - auch Atomic-Operations.

    BTW: ich hab gerade nachgeguckt. Boost (1.45) verwendet einfach ne Condition-Variable + Mutex. Allerdings hat deren Implementierung eine "spurious wakeup" Sicherung drinnen 😉


Anmelden zum Antworten