OS: Exercise 4: Synchronization and Locking

You can do this exercise in any size group you wish. Don't worry if you find some of the questions difficult to answer, since they may be intended to initiate a discussion rather than have a single correct answer.

There might also be more questions than you have time to answer, but do as many as you can.

Preparations

Before this exercise, you should (ideally) have:

Uppgift 1

Här är programmet threads-1.c, som startar två trådar, och där varje tråd ökar variabeln data med ett en miljon gånger. Slutresultatet borde förstås bli att variabeln innehåller värdet två miljoner.

// 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.

Uppgift 2

Här är C-programmet twothreads.c:

#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

Hur lång tid tar körningen?

Uppgift 3

Här är C-programmet yo-yo.c, som flyttar en etta fram och tillbaka mellan variablerna data_1 och data_2. Tråd 1 minskar data_1 och ökar data_2, medan tråd 2 minskar data_2 och ökar data_1.

// 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?

Uppgift 4

a) Vad menas spinlock och busy-wait (även kallat spinning)?

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?


Thomas Padron-McCarthy (thomas.padron-mccarthy@oru.se), May 16, 2022