Bonjour, j'ai essayé de faire un programme en programmation parallèle où genre je lance 2 threads de la même fonction avec un paramètre différent mais est-ce qu'ils s'executent en même temps de la façon dont je l'ai écrit ?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
void *monthread(void *arg)
{
char c;
int r;
printf("Thread is in execution, fuck me rightt\n"); // this line is executed ! why not the others ?
c = getchar();
scanf("%d", &r);
while(1)
{
//c = 'e';
printf("loop %d\n", r);
}
(void) arg; // and can someone explain me this line please ? i was reading a tutorial
//pthread_exit(NULL); // and it says to add this but why ? what does it do ? thanks
}
int main(void){
int i;
pthread_t thread_h;
pthread_t thread_h2;
printf("Thread creation in 3 2 1\n");
if(pthread_create(&thread_h, NULL, monthread, NULL) == -1){
perror("pthread_create");
return EXIT_FAILURE;
}
if(pthread_create(&thread_h2, NULL, monthread, NULL) == -1){
perror("pthread_create");
return EXIT_FAILURE;
}
pthread_join(thread_h, NULL);
pthread_join(thread_h2, NULL);
//i = getchar();
printf("thread created\n");
/*while (1){
i = 0;
}*/
}
genre là j'ai l'affichage que du paramètre de mon 2ème thread c'est normal ??
calistas, comme d'hab avec les threads et les process, ils s'executent quand le systeme a bien envie de les executer. Ils peuvent tourner en meme temps ou ne pas tourner en meme temps. C'est le systeme qui choisit ce qu'il a envie de faire.
Dans ton cas, les deux threads peuvent tourner en meme temps. Faire des io sur stdin dans des threads different, ca ressemble a une tres mauvaise idee.
pthread exit ca ne sert a rien.
le cast en void de arg, c'est juste un truc debile pour retirer un warning du compilateur.
C'est quoi ce tuto que tu lis que je tiennes mes etudiants loin de lui ?
aucun j'ai juste lu le tuto sur les threads mais c'était pas POUR faire du parallélisme, j'ai juste essayé d'en faire un moi-même
j'ai remarqué que tu aides souvent très peu dans tes réponses et tu te moques surtout beaucoup, comme si tu étais né en sachant programmer, je te blackliste donc car à chaque fois que je crois avoir une réponse je revois encore ton pseudo, répondant inlassablement par un message qui m'est au final absolument inutile
Et si tu enlèves ton c=getchar et là ligne suivante, ça fonctionne ? Je ne peux pas tester là, et je n'ai pas fait énormément de threads en c avec lpthread.
Car au final, tant que tu n'entres pas de caractère tu ne peux pas accéder à ton while.
Tes deux threads se lancent super vite, tu en as donc probablement un qui bloque sur cette instruction, et comme les threads ça fonctionne au bon vouloir de l'ordonnanceur, tu ne sais jamais vraiment à quel thread tu as affaire dans ce genre de cas.
Essayes de mettre un sleep entre le lancement de tes threads et la lecture de ton caractère sinon, ou pose des locks.
Sinon si tu veux savoir si les deux threads tournent simultanément, lance un truc bidon genre le gestionnaire des tâches windows et regarde la charge de tes coeurs. Ca marchera, sauf si ton thread bloque, bien sûr
+ pour ta réponse à Godrik, ce n'est pas pour le défendre mais au moins il pousse à chercher des solutions en donnant des indices. C'est sûr, c'est moins rapide que de faire des c/c de stackoverflow mais au moins ça aide à éviter des problèmes similaires à l'avenir.
calistas, je ne comprends pas bien ton message. J'ai l'impression que mon message precedent repond exactement aux questions poses:
-tu demandes si les threasd tournent en meme temps. Je reponds que ca depend du systeme, mais qu'ils peuvent tourner en meme temps.
-le code demande des explicatins sur "pthread_exit(NULL);", je reponds que ca sert a rien.
-le code demande des explications sur le cas "(void) args;", je reponds que c'est un truc pour contourner un warning du compilateur.
-J'indique que tes threads font des io sur stdin et que c'est une mauvaise idee. Comme tu n'as pas l'air de bien connaitre l'utilisation des threads, j'ai pas voulu rentrer dans les details. Mais la "thread safety" des io sur un seul descripteur de fichier n'a pas ete clair pendant longtemps. Ca a ete implementation dependant pendant TRES longtemps, et ca n'a ete clarifie que recement.
-Ton code mentionne un tutorial, et visiblement tu n'arrives pas a le suivre. Donc le tuto ne doit pas etre clair. Est ce que ce n'est pas raisonnable de demander lequel tu suis afin de ne pas le recommender a l'avenir?
J'ai repondu a toutes les question pose sauf une. Pourquoi est ce que ce message n'est utile ?
Comme tout le monde l'a dit : (void) arg; sert à retirer un warning compilateur.
Pourquoi ? Car ton thread en entrée possède un argument : void *arg, mais il n'est pas utilisé dans le thread donc le compilateur va te sortir un message du style "attention, tu as une variable initialisée mais non utilisée".
pthread_exit(NULL); Aucune idée.
Sinon je vois que tu utilises la fonction "getchar()". Cette fonction est bloquante dans le sens où elle va attendre qu'un utilisateur tape quelque chose dans la fenêtre de commande.
Si tu as tes deux threads qui tournent en parallèle et qui tentent d'accéder à la même ressource ça va poser des problèmes. Dans ce cas là il vaut mieux utiliser des semaphores.
Les opérations sur les fichiers sont atomiques mais printf utilise probablement un tampon qui n'a pas de verrou.
Ta lit la position du tampon
Tb lit la position du tampon
* Ta et Tb ont lu la même position *
Ta incrémente la position du tampon
Tb incrémente la position du tampon
Ta écrit dans le tampon
Tb écrit dans le tampon a la même position et efface ce que Ta écrit.
Un solution serait d’utilisé sprintf dans un tampon propre a chaque thread et d'ensuite utiliser fwrite avec stdout
int length = sprintf(NULL, "loop %d\n", r);
char buffer[length];
sprintf(buffer, "loop %d\n", r);
fwrite(buffer, 1, length, stdout); // atomique
Je m'incruste pour poser une question, il y a un truc que je suis pas sur d'avoir compris avec les opérations atomique, qu'est ce qui empêche deux opérations atomiques d'être exécutées en même temps via deux threads différents sur deux cœurs différents ?
Dans un système avec un seul cœur je comprends, l'opération est effectuée d'un trait donc elle sera forcément terminée avant que les autres opérations soient exécutées, mais dans le cas d'un système possédant plusieurs cœurs je comprends pas trop comment ça se passe.
EDIT: Après quand je parle d'opérations atomiques personnellement je parle surtout des std::atomic dans la std lib du C++.
(Ah, bah tiens, j'enseignais ca la semaine derniere.)
lokilok, ca depend de ce que tu appelle atomic. Mais en l'occurence tu parles des std::atomic de C++.
les atomic de C++ mappent a des instructions du processeurs qui font exactement ca. Il y a des instructions comme compare_and_swap(A,oldval, newval) qui assurent de faire atomiquement:{
bool cnd = (*A == oldval);
if (cnd)
*A = newval;
return cnd;
}
Le truc est que l'instruction est encode directement dans le processeur. Donc le processeur garantie l'atomicite.
Si tu es plus curieux, les processeur intel garantissent l'atomicite de l'operation en mettant un genre de lock sur une ligne de cache. qui garanti que le core a un access exclusif.
https://en.wikipedia.org/wiki/Test-and-set
https://en.wikipedia.org/wiki/Compare-and-swap
https://en.wikipedia.org/wiki/Fetch-and-add
Dans les atomic C++ tu as aussi des histores de consistence memoire qui sont garantie avec els sfence, mfence, et je-sais-plus-quoi-fence.
Ah ok, je vois mieux comment ça fonctionne merci.
avec x86, on peut exécuter une instruction atomiquement en la préfixant avec LOCK.
ADD [adresse], 123 ; pas atomique
LOCK ADD [adresse], 123 ; atomique
Les processeurs d'intel et d'amd on un cache global qui est partagé par tous les cœurs (L3 sur intel i7).
Chaque ligne d'adresse a un verrou dont la valeur est soit 0 (désactivé), soit le numéro du coeur détenteur (donc 3 bits pour pour un cpu 4 cœurs).
Maintenant il y a un truc qui me turlupine, godrik. si je prends par exemple l'instruction ADD-to-memory de ci dessus, je peux la décomposer en micro opérations suivantes
FETCH register, address ; register := cache[address]
ADD register, operand ; register :+= operand
STORE address, register ; cache[address] := register
maintenant, si j'ai
CORE #1 CORE #2
ADD [0xABCD], 123 LOCK ADD [0xABCD], 456
et que CORE #2 a un cycle de retard sur CORE #1
CORE #1 CORE #2
CYCLE 1 FETCH R0, 0xABCD ...
CYCLE 2 ADD R0, 123 LOCK 0xABCD
CYCLE 3 STALL (LOCKED) FETCH R0, 0xABCD
CYCLE 4 STALL (LOCKED) ADD R0, 456
CYCLE 5 STALL (LOCKED) STORE 0xABCD, R0
CYCLE 6 STALL (LOCKED) UNLOCK 0xABCD
CYCLE 7 STORE 0xABCD, R0 ...
il y a quand même un data race malgré l'utilisation de LOCK.
CodeArtisan, mais dans quel cas concret il serait utile de pouvoir faire des opérations atomique et non atomique sur un objet ? Avec les std::atomic par exemple tu ne peux que faire des opérations atomiques, donc ton cas de figure ne peut pas se présenter.
codeartisan, de memoire fetch_and_add est une seule instruction. Ou alors je n'ai pas compris ce que tu veux dire.
Ah, je viens de relire ce que tu disais. Tu parlais au niveau du microcode.
Tu as un core qui faire un fetch_and_add et l'autre qui fait un add non synchro. Donc en effet, tu vas avoir des une data race. Mais pourquoi faire ca ?
Souvent quand tu as un atomic et une autre operation non atomique, l'operation non atomique est souvent un read.
normalement l'effet d'une instruction atomique doit être vu par tous les processeurs or ici ce n'est pas le cas: core #0 ignore l'effet produit par core #1. J'ai regardé dans le manuel d'intel et j'ai trouvé
When one processor accesses data cached on another processor, it must not receive incorrect data. If it modifies data, all other processors that access that data must receive the modified data.
[...]
For the P6 family processors, locked operations serialize all outstanding load and store operations (that is, wait for them to complete). This rule is also true for the Pentium 4 and Intel Xeon processors, with one exception. Load operations that reference weakly ordered memory types (such as the WC memory type) may not be serialized.
En gros, chaque lock est précédé par un vidage (flush) des files d’attentes des processeurs. Je vais essayer d'avoir un preuve empirique.
Je ne vois pas en quoi core0 a ignore l'effet produit par core 1. Il a lu la donnee avant l'atomique. Et l'ecrit apres.
après avoir testé, je peux confirmer qu'il y a bien un data race si on mélange lock et non lock.
Evidement ;)
même si on a besoin que d'un bit, il vaut mieux avoir un verrou d'une taille de 64 octets (taille d'une ligne de cache)
avec struct { uint lock; uint data; }
Performance counter stats for './a.out':
11428.232641 task-clock:u (msec)
0 context-switches:u
0 cpu-migrations:u
57 page-faults:u
34,265,341,045 cycles:u
3,999,658,957 instructions:u
2,000,521,923 branches:u
343 branch-misses:u
9.339586720 seconds time elapsed
avec struct { uint lock; char pad[64]; uint data; }
Performance counter stats for './a.out':
9877.843435 task-clock:u (msec)
0 context-switches:u
0 cpu-migrations:u
59 page-faults:u
29,607,404,289 cycles:u
4,000,218,402 instructions:u
2,000,631,941 branches:u
301 branch-misses:u
7.887226292 seconds time elapsed
±même nombre d'instructions mais 15% plus rapide
Et c'est quoi le code que tu testes ?