PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Frage zu pthread_testcancel();



HaWe
24.10.2016, 13:07
hallo,
ich bin noch dabei, für den Fall von Notstopp-Events meinen Robot sicher und schnell zum Stehen zu bringen und auch langdauernde Threads sicher und schnell abbrechen zu können.

Beispielhaft folgendes vereinfachte Setup:

main() initialisiert 2 pthreads, nämlich threadname1() und threadname2().

einer davon startet, sobald er aufgerufen wird, eine rekursive Funktion, die sehr viel Rechenzeit erfordert (z.B. einen AStern).

void AStern(int x, int y, int xz, int yz) {
// tutwas
// ruft sich selber auf
Astern(x+1, y+1, 100,90);

// tut wieder was bis zur Abbruchbedingung
}

void *threadname2 (void *arg) {
printf("starte Astern\n");
AStern(0,0, 100,90);
return NULL;
}

es macht hier sicher nur wenig Sinn, pthread_testcancel() im pthread threadname2() auftzurufen, da ja nach Start sofort die Funktion AStern() angesprungen wird.
Macht es aber Sinn, in der Funktion AStern() z.B. ganz am Anfang diesen Befehl zu setzen uns ggf. auch im Code zwischendurch auch nochmal, damit öfter (z.B. in jeder rekursiven Instanz) auf Abbruch von threadname2 getestet wird?

markusj
24.10.2016, 20:05
Macht es aber Sinn, in der Funktion AStern() z.B. ganz am Anfang diesen Befehl zu setzen uns ggf. auch im Code zwischendurch auch nochmal, damit öfter (z.B. in jeder rekursiven Instanz) auf Abbruch von threadname2 getestet wird?

Ja, genau so ist das gedacht. Vielleicht willst du eine Funktion "pollEvents()" o.ä. definieren, die regelmäßig aufgerufen werden soll, und in der du dann pthread_testcancel() ausführst und außerdem noch alles andere was vielleicht so gelegentlich Mal an Ereignissen abgearbeitet werden soll.

Grüße,
Markus

HaWe
24.10.2016, 21:17
prima, danke für die Rückmeldung!
Ich hatte befürchtet, pthread_testcancel() würde vlt nur direkt in dem pthread-Thread wirken, in dem der extern ausgelöste pthread_cancel() wirken soll, also "direkt" im Funktionskörper von *threadname2 () ,
evtl aber vlt nicht mehr in nachgeordneten Funktionen, die ihrerseits nur von dem pthread sekundär angesprungen wurden (und vlt ja nicht "wissen" über die pthread-ID, die hier ggf gemeint bzw. betroffen ist, hier in AStern().
Wie das AStern "merken soll", welcher übergeordnete Thread beendet werden soll (und damit auch AStern selber) ist mir allerdings noch nicht klar - aber gut, wenn es dann wenigstens funktioniert wie geplant 8)

markusj
25.10.2016, 20:55
pthread_testcancel() interessiert sich nicht dafür, welche andere Funktion sie aufruft. Jeder Thread hat gewisse Kontextinformationen, und auf diese wird zugegriffen. Du bekommst also immer die Information zu dem Thread der deine Funktion gerade jetzt ausführt. Wenn deine Funktion von zwei Threads ausgeführt wird, kann das Ergebnis also durchaus einmal TRUE und einmal FALSE sein!

Grüße,
Markus

HaWe
25.10.2016, 22:56
neinnein, jede die es betrifft, wird nur 1x von 1 Thread aus angesprungen.
Ich habe mal eine kleine Simulation geschrieben:

Einer der Threads startet eine Fibonacci-Berechnung linear (sehr schnell),
der andere eine andere, rekursive Fibinacci-Funktion (viel langsamer).

lässt man jetzt die Fibonacci (93) berechnen, ist Thread2-linear schnell fertig (nur zu Simulationszwecken),
Thread1-rekursiv braucht aber ewig.

beide Threads können durch einen 3. Thread abgebrochen werden, wenn man ESC drückt (um Thread2-linear zu erwischen, irre schnell ... ;) ).

Aber egal, ob jetzt in Fibo-Rekursiv ein pthread_testcancel drin steht oder nicht:
es dauert immer gleich lang bis zum Abbruch des rekursiven , nämlich (bei meinem Linux) ca. 3 sec.

Eigentlich ist das zu lang, und es müsste auch viel schneller gehen, da sich jede Sekunde mindestens 100x die Funktion selber rekursiv aufruft, und daher theoretisch also auch innerhalb von 10ms abgebrochen werden müsste mit pthread_testcancel() am Anfang eines jeden neuen (rekursiven) Funktionsstarts
- fast ewig lang allein bis zur Reaktion auf den Abbruch, vergleicht man es mit dem kompletten Durchlauf der gesamten kompletten linearen Funktion bis zur 200. Iteration, erkennbar auch an der ausgegeben printf-ESC-Sequenz im Terminal bis zur letztendlichen Abbruch-Reaktion.

Sehr effektiv, gelinde gesagt, scheint also pthread_testcancel in der "abhängigen" rekursiven Funktion nicht zu wirken... ;)


/*
* Fibonacci numbers
* simulaneous competition:
* linear vs. recursive calculation
*
*/


#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <termio.h>

#define msleep(ms) usleep(1000*ms)

pthread_t threadID0, threadID1, threadID2;

long f;

volatile int running1 = 1;
volatile int running2 = 1;


// ************************************************** ******************


bool kbhit(void)
{
struct termios original;
tcgetattr(STDIN_FILENO, &original);

struct termios term;
memcpy(&term, &original, sizeof(term));
term.c_lflag &= ~ICANON;
tcsetattr(STDIN_FILENO, TCSANOW, &term);

int characters_buffered = 0;
ioctl(STDIN_FILENO, FIONREAD, &characters_buffered);
tcsetattr(STDIN_FILENO, TCSANOW, &original);

bool pressed = (characters_buffered != 0);

return pressed;
}

// ************************************************** ******************


inline unsigned long long fibolinear( long n ) {
volatile static unsigned long long fibarr[3];
static long i;

fibarr[0]=0;
fibarr[1]=1;
fibarr[2]=0;

if(n==0) return 0;

for (i=1; i <= n; i++) {

fibarr[0]=fibarr[1];
fibarr[1]=fibarr[2];
fibarr[2]=fibarr[1]+fibarr[0];
printf("\n order linear: %ld %llu \n" , i, fibarr[2]);
}
return fibarr[2];

}



inline unsigned long long fiborecurs( long n ) {
unsigned long long llbuf;
volatile static long order = 0;

pthread_testcancel(); // <<<<<<<<<<<<<<<<<<<< oder mal weglassen!

if ( n==0 ) { llbuf = 0; }
else
if ( n==1 ) { llbuf = 1; }
else {

llbuf = fiborecurs(n-1) + fiborecurs(n-2);
if(n > order) {
order = n;
printf("\n order recursive: %ld %llu \n" , order, llbuf);
}
}
return (llbuf);
}



// ************************************************** ******************




void* threadf2(void *) {
unsigned long long res;
int RETVAL = 0;

res = fibolinear(f);

printf("\n\nFibonacci (%ld) (linear) is = %llu \n", f, res);
printf("===============================================\n\ n");

running2 = 0;
msleep(100);

return (void*)RETVAL; ;

}



void* threadf1(void *) {
unsigned long long res;
int RETVAL = 0;

res = fiborecurs(f);

printf("\n\nFibonacci (%ld) (recursive) is = %llu \n", f, res);
printf("===============================================\n\ n");

running1 = 0;
msleep(100);

return (void*)RETVAL; ;

}




void* threadW0(void *) {
int RETVAL = 0;

while(running1 || running2) {

if (kbhit())
{
int ch = getchar();
if(ch==27) {
// DEBUG
printf("\nin pthread0: user kbd press ESC... ");
printf("\npthread0 cancelling pthread 1... ");
if(threadID1) pthread_cancel(threadID1);

// DEBUG
printf("\npthread0 cancelling pthread 2...");
if(threadID2) pthread_cancel(threadID2);

running1 = 0;
running2 = 0;

// DEBUG
printf("\nin pthread0: program interrupted by user \n");

RETVAL = 27; // user break
return (void*)RETVAL;
}
}
msleep(1000);
printf(".");
fflush(stdout);
}
return (void*)RETVAL;

}

// ************************************************** ******************


int main() {
void *retval0 = NULL, *retval1 = NULL, *retval2 = NULL;

printf("enter Fibonacci order: ");
scanf("%ld", &f);

pthread_create( &threadID1, 0, threadf1, 0 );
pthread_create( &threadID2, 0, threadf2, 0 );
pthread_create( &threadID0, 0, threadW0, 0 );


pthread_join( threadID0, &retval0 );
pthread_join( threadID1, &retval1 );
pthread_join( threadID2, &retval2 );

// DEBUG
printf("\nretval0 = %d \n", (int)retval0);
printf("\nretval1 = %d \n", (int)retval1);
printf("\nretval2 = %d \n", (int)retval2);
printf("\nprogram finished \n");

return 0;

}

botty
26.10.2016, 01:19
Das liegt aber nicht an pthread_testcancel() sondern an deinem msleep()-Makro, welches den Test auf einen Tastendruck nur einmal in einer Sekunde ausführen läßt.
Reduzier das doch mal auf 1ms - mal sehen was dann raus kommt.

Gruss botty

HaWe
26.10.2016, 07:44
Ja, du hast Recht, da hatte ich was falsch interpretiert:

In den printf-Ausgaben kommt meistens ziemlich schnell nach ESC-Tastendruck das ESC als Sequenz
^[
auf den terminal-Screen.
Ich dachte, das wäre identisch mit dem Zeitpunkt, in dem auch kbhit() und getchar() das ESC lesen (dass die also für das Echo verantwortlich wären), wodurch es dann auf stdout ausgegeben wird, und dann wird ja ohne Verzögerung (keine 1000ms später, sondern sofort!) in die if-Schleife eingesprungen und das pthread_cancel dann auch sofort ausgelöst.

Sehen konnte man aber, dass zwischen ^[ und dem Programmende noch etliche Zeilen von (zig) rekursiven Fibonacci-Berechnungen ausgeführt und ausgegeben werden.

Nach dem Test mit msleep(10) kommt der Abbruch allerdings jetzt wirklich unmittelbar.

Tatsächlich scheint es dann aber so zu sein, dass das kbd-Echo ^[ vom ESC-Tastendruck schneller kommt, als kbhit() oder getchar() davon Kenntnis kriegen, und daher habe ich diese Verzögerung völlig falsch interpretiert. Offenbar macht das kbd-Echo ein von meinem Programm völlig unabhängger Linux-Betriebssystem-Thread oder Daemon, oder wie ist das zu erklären?

botty
26.10.2016, 12:27
Hi,

das Kernthema bei der Verarbeitung deiner Tastatureingaben ist, dass sich der Algo grundsätzlich geändert hat.
Als du noch Single-Threaded programmiert hast, hattest du eine Endlosschleife, die mehrere Sachen abarbeiten sollte und eine war davon die Tastureingabe. Da die getc Varianten blockierende Funktionen sind, musstest du erst testen ob denn wirklich ein Zeichen vorliegt, das hat kbdhit() für dich erledigt. Erst dann konntest du Lesen, damit der Rest des Systems nicht zum Stehen kommt.
Jetzt wo du einen separaten Thread für die Taststureingabe hast, führt dies zum Pollen und damit zum unnötigen Verbraten von Rechenzeit. Jetzt muss ein getchar() blockieren, unabhängig davon was die anderen Threads machen.

Der zweite Aspekt ist, dass unter Linux stdio Line-Buffered ist, soll heißen, dass erst nach einem Return (und damit Line-Feed) die Zeichen die im Puffer stehen an getc und Konsorten weitegereicht werden.
Du willst aber auf jedes Zeichen sofort reagieren können und so muss man mit Hilfe des Termio-APIs ein wenig nachhelfen:


void* threadW0(void *arg) {
int RETVAL = 0;
int ch = 0;
struct termios original, term;

tcgetattr(STDIN_FILENO, &original);

memcpy(&term, &original, sizeof(term));
term.c_lflag &= ~ICANON;
tcsetattr(STDIN_FILENO, TCSANOW, &term);

while(true) {

ch = getchar();

if(ch == 27) {
// DEBUG
printf("\nin pthread0: user kbd press ESC... ");
printf("\npthread0 cancelling pthread 1... ");
if(threadID1) pthread_cancel(threadID1);

// DEBUG
printf("\npthread0 cancelling pthread 2...");
if(threadID2) pthread_cancel(threadID2);

running1 = 0;
running2 = 0;

// DEBUG
printf("\nin pthread0: program interrupted by user \n");

// Terminal zuruecksetzen
tcsetattr(STDIN_FILENO, TCSANOW, &original);

RETVAL = 27; // user break
return (void*)RETVAL;
}
}

return (void*)RETVAL;

}

Du setzt das mit dem Terminal gemeinsam benutzte stdio in den non-caninonical Mode, was dir sofort die Zeichen durchreicht und nicht erst auf ein Return wartet.
Wichtig ist, dass das zurück gesetzt wird, bevor sich dein Programm beendet, sonst ist das Terminal zerschossen.
Das Echo, wenn du die Tasten drückst, kommt trotzdem noch vom Terminal her aber die Zeitdifferenz ist jetzt weg, da dieser Thread in getchar() blockiert und sobald ein Zeichen verfügbar ist, einerseits das Terminal es anzeigt, andererseits aber dein Thread aufgeweckt wird und seine Verabeitung machen kann.

Eine ganz andere Sache ist beim Cancel von Threads, dass du darauf achten musst, dass deine Daten konsistent bleiben. Das heisst, dass du genau hinschauen musst welche Library-Calls, System-Calls oder phtread_testcancel() einen Cancelpoint erzeugen und ob an dieser Stelle nicht Inkonsistenzen erzeugt werden, wenn ein Cancel stattfinden würde.
Ein ungetestet Beispiel:
fopen, fprintf, und fclose können Cancelpoints einfügen. Wenn du jetzt z.B. ein Rechenergebnis in eine Darei schreiben möchtest, dann funktioniert dies unter Umständen nicht mehr:


FILE *fp = NULL;

fp = fopen("data.txt", "w");

fprintf("Fib von %ld ist %ld\n", 100, fiborecurs(100));

fclose(fp)

Nehmen wir an, der Thread läuft durch fopen durch und auch fiborecurs wird angeworfen. Dann ist fp ein offener Dateideskriptor. Wenn jetzt ein anderer Thread ein Cancel anfordert und der Thread, der diese Codepassage enthält entweder in fiborecurs oder fprintf gecancelt wird, bleibt ein offener Dateideskriptor zurück, da fclose nicht mehr erreicht wird.
Wenn du das ein paar Mal machst, wird irgendwann die Dateideskriptortabelle des Programms überlaufen und das Betriebsystem wird mit einem Fehler und Beendigung des Programms reagieren (oder es passieren andere spannende Sachen ;) ).
In solchen Passagen musst du ein Cancel unterbinden, also sicher sein das deine Lib- und Sys-Calls keine Cancelpoints sind (man pthreads ist da eine Anlaufstelle) oder du mußt mit pthread_setcancelstate() zeitweise Canceln unterbinden und dann wieder anschalten. Du wirst vermutlich deinen gesamten Code unter diesem Aspekt einer genauen Betrachtung unterziehen müssen.

Als letzt Bemerkung: fiborecurs ist nicht Multi-Thread fähig,da die Variable order dank des static im Datensegment liegt. Würden mehrere Threads diese Funktion ausführen, gäbs Kuddel-Muddel. Grundsätzlich ist es eine sichere Herangehensweise, derartige Variablen zu vermeiden (oder sehr genau zu wissen warum die da sind und in einem Kommentar über die Funktion zu schreiben, dass sie nicht Multi-Thread fähig ist!).

Gruss botty

HaWe
26.10.2016, 12:52
dankeschön für die sehr ausführliche Erklärung, wenn ich auch gestehen muss, jetzt ist es wirklich sääähr schwäääre Kost ;)

das mit nicht-blockierendem kbhit() habe ich machen müssen, damit er mir im Sekundentakt Punkte auf den Bildschirm schreibt. Fibo rekursiv ab 50 aufwärts dauert Stunden (!), und daher die Punkte, damit man weiß, dass er noch lebt ;)

Den Rest werde ich bei mir verlinken und speichern unter

"Wichtig! Nicht erledigt! (eilt net ganz so!)" :D

Danke nochmal! :)


edit,
BTW:
weißt du, wie man eine long long unsigned Variable auf Überlauf testet? :confused:

botty
26.10.2016, 16:29
Bei unsigned gilt

Addition:


/* a + b */
ULLONG_MAX - a < b ==> Ueberlauf


Subtraktion


/* a - b */
a < b ==> Unterlauf


Multiplikation:


/* a * b */
a > ULLONG_MAX / b ==> Ueberlauf


Wenn die linke Seite vom "==>" wahr wird folgt rechte Seite.


Division: Test das Divisor > 0 ist. Und wenn Divisor > Dividend gibt's 'ne 0

Das sind die Fälle die mir einfallen und die Tests müssen vor der eigentlichen Operation gemacht werden.

Gruss botty

HaWe
26.10.2016, 17:06
ja, genau, das hatte ich auch befürchtet, ich hatte gehofft, der Prozessor könnte es in seiner Ganzzahl-Arithmetik-Unit selber von sich aus feststellen. Aber das wird dann jetzt hier eher OT.