Disabled external gits

This commit is contained in:
2022-04-07 18:46:57 +02:00
parent 88cb3426ad
commit 15e7120d6d
5316 changed files with 4563444 additions and 6 deletions

View File

@@ -0,0 +1,15 @@
LDFLAGS += -pthread
BIN=counter1 counter2 counter3 counter4 election1 election2 election3 election4 procon1 procon2 procon3 procon4
all: ${BIN}
.PHONY: all
clean:
rm *.o ${BIN}
.PHONY: clean
counter2: lock.o
election2: lock.o
procon2: lock.o
procon4: lock.o

View File

@@ -0,0 +1,73 @@
# Simple synchronization examples.
Modifying a variable that is concurrently accessed by another thread is
dangerous and will (most of the time) lead to Undefined Behaviors and Data
Races.
Through simple examples, we'll see how to use synchronization primitives to
build thread-safe programs using locks and atomic variables.
We build 3 examples:
- A counter. Each thread increments a shared counter repeatedly RUNS times.
We check that the counter = RUNS * THREADS at the end.
- Leader election. In each run, one thread is elected as the leader. We check
that exactly one thread considers itself as the leader in each run.
- Producer-Consumer. One thread generates data in a circular buffer while the
second tries to consume it. They have to be kept in sync.
These examples also show how to start threads and wait for their completion.
YOU DON'T HAVE TO START THREADS YOURSELVES IN THE DUAL-VERSIONED STM ALGO.
In the STM, threads are started by the users of your library (in which they
repeatedly (1) start a transaction, (2) read/write/alloc/free STM memory and (3)
commit the transaction.).
## Counter
### Bad approach #1
We employ no synchronization primitive: Undefined Behavior (UB), bad bad.
### Good approach #1
We take a big nice lock around the ++: correct.
### Bad approach #2
We use an atomic variable but use 2 non atomic operations: not UB, but doesn't
work.
### Good approach #2
We use an atomic variable and an atomic operation (fetch and add): correct.
## Leader election
### Bad approch # 1
We employ no synchronization primitive: Undefined Behavior (UB), bad bad.
### Good approach #1
We take a big nice lock around the test and set: correct.
### Bad approach #2
We use an atomic variable but use 2 non atomic operations: not UB, but doesn't
work.
### Good approach #2
We use an atomic variable and an atomic operation (compare and swap): correct.
## Producer-consumer
### Bad approach
We employ no synchronization primitive: Undefined Behavior (UB), bad bad.
### Okayish approach #1
We take a big nice lock when when writting/reading and checking the bounds:
correct.
### Okayish approach #2
We use atomic variables and fences: correct.
### Good approach
We use a conditional variable. :)
https://www.ibm.com/docs/en/i/7.1?topic=ssw_ibm_i_71/apis/users_78.htm
Conditional variables are synchronization primitives provided by the kernel
that let a thread sleep until it's woken up. Using a "cond var", a consummer
that realizes that data has not been generated yet can go to sleep instead of
busy waiting. It will then be woken up by the producer once the data is
generated. :)

View File

@@ -0,0 +1,42 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#define RUNS (4096 * 256)
#define THREADS 4
static int counter = 0;
void* count(void* null) {
for (int r = 0; r < RUNS; r++) {
counter++;
// Equivalent to:
//
// int local = counter;
// counter = local + 1;
//
// This is not thread-safe as the increment is not atomic.
// **Incorrect**
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, count, NULL);
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
if (counter != RUNS * THREADS) {
printf("Didn't count so well. :/, found %d\n", counter);
} else {
printf("Counted up to %d.\n", counter);
}
}

View File

@@ -0,0 +1,45 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include "lock.h"
#define RUNS (4096 * 256)
#define THREADS 4
static struct lock_t lock;
static int counter = 0;
void* count(void* null) {
for (int r = 0; r < RUNS; r++) {
lock_acquire(&lock);
counter++;
lock_release(&lock);
// Still equivalent, but only one thread accesses counter at a time, so it's
// thread safe, no concurrent access/data race. :)
// **Correct**
}
}
int main() {
lock_init(&lock);
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, count, NULL);
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
if (counter != RUNS * THREADS) {
printf("Didn't count so well. :/, found %d\n", counter);
} else {
printf("Counted up to %d.\n", counter);
}
}

View File

@@ -0,0 +1,38 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#define RUNS (4096 * 256)
#define THREADS 4
static atomic_int counter = 0;
void* count(void* null) {
for (int r = 0; r < RUNS; r++) {
int read_copy = counter;
counter = read_copy + 1;
// **Incorrect**, using atomic variables (~atomic registers is not enough).
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, count, NULL);
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
if (counter != RUNS * THREADS) {
printf("Didn't count so well. :/, found %d\n", counter);
} else {
printf("Counted up to %d.\n", counter);
}
}

View File

@@ -0,0 +1,37 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#define RUNS (4096 * 256)
#define THREADS 4
static atomic_int counter = 0;
void* count(void* null) {
for (int r = 0; r < RUNS; r++) {
atomic_fetch_add(&counter, 1);
// **Correct**, using an atomic operation (most likely) provided by the CPU.
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, count, NULL);
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
if (counter != RUNS * THREADS) {
printf("Didn't count so well. :/, found %d\n", counter);
} else {
printf("Counted up to %d.\n", counter);
}
}

View File

@@ -0,0 +1,48 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#define RUNS (4096 * 256)
#define THREADS 4
static int leader[RUNS] = { 0 };
static atomic_int nb_leaders[RUNS] = { 0 }; // used to check correctness
void* elect(void* _tid) {
intptr_t tid = (intptr_t) _tid;
// For each run (or round) we try to impose ourselves as the run's leader.
for (int r = 0; r < RUNS; r++) {
if (leader[r] == 0) {
leader[r] = tid;
atomic_fetch_add(&nb_leaders[r], 1); // used to check correctness
}
// **Incorrect**, super wrong, plz synchronize.
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, elect, (void*)(i + 1));
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
int r = 0;
for (; r < RUNS; r++) {
if (nb_leaders[r] != 1) {
printf("Leader election for round %d failed.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,54 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#include "lock.h"
#define RUNS (4096 * 256)
#define THREADS 4
static struct lock_t lock;
static int leader[RUNS] = { 0 };
static atomic_int nb_leaders[RUNS] = { 0 }; // used to check correctness
void* elect(void* _tid) {
intptr_t tid = (intptr_t) _tid;
for (int r = 0; r < RUNS; r++) {
lock_acquire(&lock);
if (leader[r] == 0) {
leader[r] = tid;
atomic_fetch_add(&nb_leaders[r], 1); // used to check correctness
}
lock_release(&lock);
// **Correct**, a single thread tries to become leader at a time.
}
}
int main() {
lock_init(&lock);
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, elect, (void*)(i + 1));
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
int r = 0;
for (; r < RUNS; r++) {
if (nb_leaders[r] != 1) {
printf("Leader election for round %d failed.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,48 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#define RUNS (4096 * 256)
#define THREADS 4
static atomic_int leader[RUNS] = { 0 };
static atomic_int nb_leaders[RUNS] = { 0 }; // used to check correctness
void* elect(void* _tid) {
intptr_t tid = (intptr_t) _tid;
for (int r = 0; r < RUNS; r++) {
if (leader[r] == 0) {
leader[r] = tid;
atomic_fetch_add(&nb_leaders[r], 1); // used to check correctness
}
// **Incorrect**, atomic variables (~atomic registers) do not enforce atomic
// operation blocks.
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, elect, (void*)(i + 1));
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
int r = 0;
for (; r < RUNS; r++) {
if (nb_leaders[r] != 1) {
printf("Leader election for round %d failed.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,48 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#define RUNS (4096 * 256)
#define THREADS 4
static atomic_int leader[RUNS] = { 0 };
static atomic_int nb_leaders[RUNS] = { 0 }; // used to check correctness
void* elect(void* _tid) {
intptr_t tid = (intptr_t) _tid;
for (int r = 0; r < RUNS; r++) {
int expected = 0;
if (atomic_compare_exchange_strong(&leader[r], &expected, tid)) {
atomic_fetch_add(&nb_leaders[r], 1); // used to check correctness
}
// **Correct**, we use the Compare and Swap operation to atomically check if
// a leader has already been elected and if not, we impose ourselves. :)
}
}
int main() {
pthread_t handlers[THREADS];
for (intptr_t i = 0; i < THREADS; i++) {
int res = pthread_create(&handlers[i], NULL, elect, (void*)(i + 1));
assert(!res);
}
for (int i = 0; i < THREADS; i++) {
int res = pthread_join(handlers[i], NULL);
assert(!res);
}
int r = 0;
for (; r < RUNS; r++) {
if (nb_leaders[r] != 1) {
printf("Leader election for round %d failed.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,27 @@
#include "lock.h"
bool lock_init(struct lock_t* lock) {
return pthread_mutex_init(&(lock->mutex), NULL) == 0
&& pthread_cond_init(&(lock->cv), NULL) == 0;
}
void lock_cleanup(struct lock_t* lock) {
pthread_mutex_destroy(&(lock->mutex));
pthread_cond_destroy(&(lock->cv));
}
bool lock_acquire(struct lock_t* lock) {
return pthread_mutex_lock(&(lock->mutex)) == 0;
}
void lock_release(struct lock_t* lock) {
pthread_mutex_unlock(&(lock->mutex));
}
void lock_wait(struct lock_t* lock) {
pthread_cond_wait(&(lock->cv), &(lock->mutex));
}
void lock_wake_up(struct lock_t* lock) {
pthread_cond_broadcast(&(lock->cv));
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <pthread.h>
#include <stdbool.h>
/**
* @brief A lock that can only be taken exclusively. Contrarily to shared locks,
* exclusive locks have wait/wake_up capabilities.
*/
struct lock_t {
pthread_mutex_t mutex;
pthread_cond_t cv;
};
/** Initialize the given lock.
* @param lock Lock to initialize
* @return Whether the operation is a success
**/
bool lock_init(struct lock_t* lock);
/** Clean up the given lock.
* @param lock Lock to clean up
**/
void lock_cleanup(struct lock_t* lock);
/** Wait and acquire the given lock.
* @param lock Lock to acquire
* @return Whether the operation is a success
**/
bool lock_acquire(struct lock_t* lock);
/** Release the given lock.
* @param lock Lock to release
**/
void lock_release(struct lock_t* lock);
/** Wait until woken up by a signal on the given lock.
* The lock is released until lock_wait completes at which point it is acquired
* again. Exclusive lock access is enforced.
* @param lock Lock to release (until woken up) and wait on.
**/
void lock_wait(struct lock_t* lock);
/** Wake up all threads waiting on the given lock.
* @param lock Lock on which other threads are waiting.
**/
void lock_wake_up(struct lock_t* lock);

View File

@@ -0,0 +1,72 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <stdbool.h>
#define RUNS 4096
#define THREADS 4
#define DATA_TEXT_SIZE 1024
#define BUFFER_SIZE 1024
struct data {
char text[DATA_TEXT_SIZE];
};
bool are_same(struct data* a, struct data* b) {
for (int i = 0; i < DATA_TEXT_SIZE; i++)
if (a->text[i] != b->text[i]) return false;
return true;
}
struct data buffer[BUFFER_SIZE] = { 0 };
struct data produced[RUNS] = { 0 }; // used to check correctness
struct data consumed[RUNS] = { 0 }; // used to check correctness
void* produce(void* null) {
for (int r = 0; r < RUNS; r++) {
for (int i = 0; i < DATA_TEXT_SIZE; i++)
produced[r].text[i] = rand();
buffer[r % BUFFER_SIZE] = produced[r];
// Super wrong: Overwritting data being read by the consumer.
}
}
void* consume(void* null) {
for (int r = 0; r < RUNS; r++) {
consumed[r] = buffer[r % BUFFER_SIZE];
// Super wrong: Reading data concurrently (or not even) written by the
// producer.
}
}
int main() {
int res;
pthread_t producer;
res = pthread_create(&producer, NULL, produce, NULL);
assert(!res);
pthread_t consumer;
res = pthread_create(&consumer, NULL, consume, NULL);
assert(!res);
res = pthread_join(consumer, NULL);
assert(!res);
res = pthread_join(producer, NULL);
assert(!res);
int r = 0;
for (; r < RUNS; r++) {
if (!are_same(&produced[r], &consumed[r])) {
printf("Consumed the wrong data on round %d.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,97 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <stdbool.h>
#include "lock.h"
#define RUNS 4096
#define THREADS 4
#define DATA_TEXT_SIZE 1024
#define BUFFER_SIZE 1024
struct data {
char text[DATA_TEXT_SIZE];
};
static struct lock_t lock;
bool are_same(struct data* a, struct data* b) {
for (int i = 0; i < DATA_TEXT_SIZE; i++)
if (a->text[i] != b->text[i]) return false;
return true;
}
struct data buffer[BUFFER_SIZE] = { 0 };
int produced_until = 0;
int consumed_until = 0;
struct data produced[RUNS] = { 0 }; // used to check correctness
struct data consumed[RUNS] = { 0 }; // used to check correctness
void* produce(void* null) {
for (int r = 0; r < RUNS; r++) {
while (true) {
lock_acquire(&lock);
if (consumed_until + BUFFER_SIZE > r) break;
lock_release(&lock);
}
printf("can produce %d\n", r);
for (int i = 0; i < DATA_TEXT_SIZE; i++)
produced[r].text[i] = rand();
buffer[r % BUFFER_SIZE] = produced[r];
produced_until++;
lock_release(&lock);
// Correct: we wait until nobody consumed data anymore and correctly release
// (prevent reordering) thanks to the lock.
}
}
void* consume(void* null) {
for (int r = 0; r < RUNS; r++) {
while (true) {
lock_acquire(&lock);
if (produced_until > r) break;
lock_release(&lock);
}
printf("can consume %d\n", r);
consumed[r] = buffer[r % BUFFER_SIZE];
consumed_until++;
lock_release(&lock);
// Correct: we wait until nobody produces data anymore and correctly acquire
// + release (prevent reordering) thanks to the lock.
}
}
int main() {
lock_init(&lock);
int res;
pthread_t producer;
res = pthread_create(&producer, NULL, produce, NULL);
assert(!res);
pthread_t consumer;
res = pthread_create(&consumer, NULL, consume, NULL);
assert(!res);
res = pthread_join(consumer, NULL);
assert(!res);
res = pthread_join(producer, NULL);
assert(!res);
int r = 0;
for (; r < RUNS; r++) {
if (!are_same(&produced[r], &consumed[r])) {
printf("Consumed the wrong data on round %d.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,91 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <stdbool.h>
#define RUNS 4096
#define THREADS 4
#define DATA_TEXT_SIZE 1024
#define BUFFER_SIZE 1024
struct data {
char text[DATA_TEXT_SIZE];
};
bool are_same(struct data* a, struct data* b) {
for (int i = 0; i < DATA_TEXT_SIZE; i++)
if (a->text[i] != b->text[i]) return false;
return true;
}
struct data buffer[BUFFER_SIZE] = { 0 };
atomic_int produced_until = 0;
atomic_int consumed_until = 0;
struct data produced[RUNS] = { 0 }; // used to check correctness
struct data consumed[RUNS] = { 0 }; // used to check correctness
void* produce(void* null) {
for (int r = 0; r < RUNS; r++) {
while (true) {
// The memory order can be relaxed as we don't read anything "produced"
// by the consumer.
int local_cu = atomic_load_explicit(&consumed_until, memory_order_relaxed);
if (local_cu + BUFFER_SIZE > r) break;
}
printf("can produce %d\n", r);
for (int i = 0; i < DATA_TEXT_SIZE; i++)
produced[r].text[i] = rand();
buffer[r % BUFFER_SIZE] = produced[r];
// We want to increment "produced_until" after the buffer has been written.
// By using memory_order_release, we prevent the STOREs on buffer from being
// reordered after the atomic operation.
atomic_fetch_add_explicit(&produced_until, 1, memory_order_release);
}
}
void* consume(void* null) {
for (int r = 0; r < RUNS; r++) {
while (true) {
// We don't want to access the buffer before checking the atomic variable.
// The memory_order_acquire prevents this reordering.
int local_pu = atomic_load_explicit(&produced_until, memory_order_acquire);
if (local_pu > r) break;
}
printf("can consume %d\n", r);
consumed[r] = buffer[r % BUFFER_SIZE];
atomic_fetch_add_explicit(&consumed_until, 1, memory_order_release);
}
}
int main() {
int res;
pthread_t producer;
res = pthread_create(&producer, NULL, produce, NULL);
assert(!res);
pthread_t consumer;
res = pthread_create(&consumer, NULL, consume, NULL);
assert(!res);
res = pthread_join(consumer, NULL);
assert(!res);
res = pthread_join(producer, NULL);
assert(!res);
int r = 0;
for (; r < RUNS; r++) {
if (!are_same(&produced[r], &consumed[r])) {
printf("Consumed the wrong data on round %d.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}

View File

@@ -0,0 +1,103 @@
#include <pthread.h>
#include <inttypes.h>
#include <assert.h>
#include <stdio.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <stdbool.h>
#include "lock.h"
#define RUNS 4096
#define THREADS 4
#define DATA_TEXT_SIZE 1024
#define BUFFER_SIZE 8
struct data {
char text[DATA_TEXT_SIZE];
};
static struct lock_t lock;
bool are_same(struct data* a, struct data* b) {
for (int i = 0; i < DATA_TEXT_SIZE; i++)
if (a->text[i] != b->text[i]) return false;
return true;
}
struct data buffer[BUFFER_SIZE] = { 0 };
int produced_until = 0;
int consumed_until = 0;
struct data produced[RUNS] = { 0 }; // used to check correctness
struct data consumed[RUNS] = { 0 }; // used to check correctness
void* produce(void* null) {
for (int r = 0; r < RUNS; r++) {
lock_acquire(&lock);
while (true) {
if (consumed_until + BUFFER_SIZE > r) break;
lock_wait(&lock);
// Note: waiting releases the lock and puts the thread to sleep. Once
// woken up, it will need to acquire back the lock to continue its
// execution.
// Said differently, if multiple threads are waiting, upon "lock_wake_up",
// only one of them will continue the execution at a time (because the
// lock is exclusive).
}
printf("can produce %d\n", r);
for (int i = 0; i < DATA_TEXT_SIZE; i++)
produced[r].text[i] = rand();
buffer[r % BUFFER_SIZE] = produced[r];
produced_until++;
lock_release(&lock);
lock_wake_up(&lock); // We tell the consumer it can continue consuming.
// Correct: This is a better version of the lock one in which we do not busy
// wait but rely on a notification primitive to let the "idle" cores rest.
}
}
void* consume(void* null) {
for (int r = 0; r < RUNS; r++) {
lock_acquire(&lock);
while (true) {
if (produced_until > r) break;
lock_wait(&lock);
}
printf("can consume %d\n", r);
consumed[r] = buffer[r % BUFFER_SIZE];
consumed_until++;
lock_release(&lock);
lock_wake_up(&lock); // We tell the producer it can continue producing.
}
}
int main() {
lock_init(&lock);
int res;
pthread_t producer;
res = pthread_create(&producer, NULL, produce, NULL);
assert(!res);
pthread_t consumer;
res = pthread_create(&consumer, NULL, consume, NULL);
assert(!res);
res = pthread_join(consumer, NULL);
assert(!res);
res = pthread_join(producer, NULL);
assert(!res);
int r = 0;
for (; r < RUNS; r++) {
if (!are_same(&produced[r], &consumed[r])) {
printf("Consumed the wrong data on round %d.\n", r);
break;
}
}
if (r == RUNS) {
printf("Looks correct to me! :)\n");
}
}