There might also be more questions than you have time to answer, but do as many as you can.
// threads-1.c #include <stdio.h> #include <pthread.h> volatile long long data = 0; void *thread_body(void *arg) { for (int i = 0; i < 1000*1000; ++i) { ++data; } return NULL; } int main(void) { pthread_t thread1, thread2; pthread_create(&thread1, NULL, thread_body, NULL); pthread_create(&thread2, NULL, thread_body, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("Result: data = %lld\n", data); } |
a) Vi kör programmet, men variabelns värde blir inte alls två miljoner, utan 1028606. Vad beror det på att det blir fel värde?
b) Vi flyttar om raderna i main-funktionen så den ser ut enligt nedan. Vad blir variabelns värde nu, och vad beror det på?
Ur programmet threads-2.c:
int main(void) { pthread_t thread1, thread2; pthread_create(&thread1, NULL, thread_body, NULL); pthread_join(thread1, NULL); pthread_create(&thread2, NULL, thread_body, NULL); pthread_join(thread2, NULL); printf("Result: data = %lld\n", data); } |
c) Kan man gissa något om hur körtiden för threads-2 kommer att bli, jämfört med threads-1? Vad beror det på? (Vi talar om den så kallade "väggklocketiden", som är den verkliga tid som det tog att köra programmen, och som man till exempel skulle kunna mäta genom att titta på en väggklocka.)
d) Vi har glömt hur man skrev för att använda pthread-paketets lås, så vi försöker göra ett lås själva. Nu ser programmet ut enligt nedan. Fungerar låset? Kommer variabelns värde att bli två miljoner nu? Förklara!
Programmet threads-3.c, med de väsentliga ändringarna markerade med rött:
// threads-3.c #include <stdio.h> #include <pthread.h> volatile long long data = 0; volatile int locked = 0; void *thread_body(void *arg) { for (int i = 0; i < 1000*1000; ++i) { // First, wait for the lock in a loop: while (locked) { } // Lock is free, lock it: locked = 1; ++data; // Release the lock: locked = 0; } return NULL; } int main(void) { pthread_t thread1, thread2; pthread_create(&thread1, NULL, thread_body, NULL); pthread_create(&thread2, NULL, thread_body, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("Result: data = %lld\n", data); } |
e) Vi gör ett nytt försök. Uppenbarligen räckte det inte att vänta till locked blir noll, och sen låsa låset, så vi provar att varje tråd skriver sitt trådnummer i locked, och sen kollar att det verkligen står rätt trådnummer där. I annat fall går vi tillbaka och försöker låsa igen. Fungerar låset? Kommer variabelns värde att bli två miljoner nu? Förklara!
Programmet threads-4.c, med de väsentliga ändringarna markerade med rött:
// threads-4.c #include <stdio.h> #include <pthread.h> volatile long long data = 0; volatile int locked = 0; void *thread_body(void *arg) { int this_thread_number = (int)arg; for (int i = 0; i < 1000*1000; ++i) { do { // First, wait for the lock in a loop: while (locked) { } // Lock is free, try to lock it: locked = this_thread_number; // ...but also check that we really did lock it! } while (locked != this_thread_number); ++data; // Release the lock: locked = 0; } return NULL; } int main(void) { pthread_t thread1, thread2; pthread_create(&thread1, NULL, thread_body, (void*)1); pthread_create(&thread2, NULL, thread_body, (void*)2); pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("Result: data = %lld\n", data); } |
f) Visa hur man gör låsningen på rätt sätt, med pthread-paketets lås.
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #define NR_THREADS 2 int total_printouts = 0; static void *thread_body(void *arg) { int thread_number = (int)arg + 1; printf("In thread %d: Started!\n", thread_number); for (int i = 0; i < 3; i++) { for (int j = 0; j < 1000000000; j++) ; printf("In thread %d, printout number %d: Running!\n", thread_number, i + 1); ++total_printouts; } printf("In thread %d: Finishing...\n", thread_number); return NULL; } // thread_body int main(void) { pthread_t threads[NR_THREADS]; printf("In main: Will be starting %d thread(s)...\n", NR_THREADS); for (int i = 0; i < NR_THREADS; ++i) { if (pthread_create(&threads[i], NULL, thread_body, (void*)i) != 0) { printf("Error: Couldn't create thread %d.\n", i); exit(EXIT_FAILURE); } } printf("In main: Waiting for %d thread(s) to finish...\n", NR_THREADS); for (int i = 0; i < NR_THREADS; ++i) { if (pthread_join(threads[i], NULL) != 0) { printf("Error: Couldn't join thread %d.\n", i); exit(EXIT_FAILURE); } } printf("In main: All %d thread(s) have finished.\n", NR_THREADS); printf("Total printouts: %d\n", total_printouts); return EXIT_SUCCESS; } // main |
Den dyraste processor som (just nu när jag skriver detta) finns i lager enligt prisjämförelsesajten Prisjakt är Intel Xeon Platinum 8280. Den kostar 141823 kronor inklusive moms. Den kan använda en terabyte (1024 gigabyte) minne och den klockas i 2,7 GHz, men den har också ett turboläge där den klarar ända upp till 4 GHz. Den har 28 kärnor, och med hyperthreading kan den köra 56 trådar parallellt.
Vi har en dator med den processorn, och Linux eller något annat modernt och bra operativsystem. Vi kör programmet på den datorn. Utmatningen från programmet kan variera lite beroende på hur trådarna råkar schemaläggas, men vid en provkörning ser det ut så här:
In main: Will be starting 2 thread(s)... In main: Waiting for 2 thread(s) to finish... In thread 1: Started! In thread 2: Started! In thread 1, printout number 1: Running! In thread 2, printout number 1: Running! In thread 1, printout number 2: Running! In thread 2, printout number 2: Running! In thread 1, printout number 3: Running! In thread 1: Finishing... In thread 2, printout number 3: Running! In thread 2: Finishing... In main: All 2 thread(s) have finished. Total printouts: 6 |
Körningen tog 4,0 sekunder.
a) Vi ändrar definitionen av NR_THREADS i programmet till 10:
#define NR_THREADS 10 |
Vad kommer nu att hända? Hur kommer utmatningen att se ut? Hur lång tid tar körningen? Förklara hur du resonerat och räknat.
b) Vi ändrar definitionen av NR_THREADS i programmet till 100:
#define NR_THREADS 100 |
Vad kommer nu att hända? Hur kommer utmatningen att se ut? Hur lång tid tar körningen? Förklara hur du resonerat och räknat.
c) Slutligen ändrar vi definitionen av NR_THREADS i programmet till 1000:
#define NR_THREADS 1000 |
// yo-yo.c #include <stdio.h> #include <pthread.h> volatile long long data_1 = 0; volatile long long data_2 = 0; void *move(void *arg) { for (int i = 0; i < 1000*1000; ++i) { data_1 = data_1 - 1; data_2 = data_2 + 1; } return NULL; } void *move_back(void *arg) { for (int i = 0; i < 1000*1000; ++i) { data_2 = data_2 - 1; data_1 = data_1 + 1; } return NULL; } int main(void) { pthread_t thread1, thread2; pthread_create(&thread1, NULL, move, NULL); pthread_create(&thread2, NULL, move_back, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("Result: data_1 = %lld, data_2 = %lld\n", data_1, data_2); } |
När programmet är klart bör både data_1 och data_2 vara tillbaka på noll.
a) Fungerar programmet som det ska? (Ledtråd: Nej.) Om inte, vad händer? Varför?
b) För att undvika att två trådar samtidigt är inne och ändrar på samma variabel, använder vi ett mutex-lås för var och en av variablerna. Här nedan visas resultatet, programmet yo-yo-locks.c. Vad händer när vi provkör det? Varför?
// yo-yo-locks.c #include <stdio.h> #include <pthread.h> volatile long long data_1 = 0; volatile long long data_2 = 0; pthread_mutex_t lock_1; // A lock for the variable data_1 pthread_mutex_t lock_2; // A lock for the variable data_2 void *move(void *arg) { for (int i = 0; i < 1000*1000; ++i) { pthread_mutex_lock(&lock_1); pthread_mutex_lock(&lock_2); data_1 = data_1 - 1; data_2 = data_2 + 1; pthread_mutex_unlock(&lock_1); pthread_mutex_unlock(&lock_2); } return NULL; } void *move_back(void *arg) { for (int i = 0; i < 1000*1000; ++i) { pthread_mutex_lock(&lock_2); pthread_mutex_lock(&lock_1); data_2 = data_2 - 1; data_1 = data_1 + 1; pthread_mutex_unlock(&lock_2); pthread_mutex_unlock(&lock_1); } return NULL; } int main(void) { pthread_t thread1, thread2; pthread_mutex_init(&lock_1, NULL); pthread_mutex_init(&lock_2, NULL); pthread_create(&thread1, NULL, move, NULL); pthread_create(&thread2, NULL, move_back, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("Result: data_1 = %lld, data_2 = %lld\n", data_1, data_2); } |
c) Beskriv några olika sätt att undvika problemet i yo-yo-locks!
d) Kunde vi använt semaforer i stället för de vanliga variablerna data_1 och data_2?
b) Ge exempel på när det kan vara lämpligt för operativsystemkärnan att använda dessa.
c) Under vilka omständigheter bör operativsystemkärnan absolut inte använda dessa?
d) Varför är det normalt inte lämpligt att använda dem i vanliga program?