Skip to content

Latest commit

 

History

History

Philosophers

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

Philosophers

Description

Item Description
Program name philo
Turn in files Makefile, *.h, *.c, in dir philo/
Makefile NAME, all, clean, fclean, re
Arguments number_of_philosophers The number of philosophers and also the number of forks.
time_to_die (in milliseconds) If a philosopher didn’t start eating time_to_die milliseconds since the beginning of their last meal or the beginning of the sim- ulation, they die.
time_to_eat (in milliseconds) The time it takes for a philosopher to eat. During that time, they will need to hold two forks.
time_to_sleep (in milliseconds) The time a philosopher will spend sleeping.
[number_of_times_each_philosopher_must_eat] (optional argument) If all philosophers have eaten at least number_of_times_each_philosopher_must_eat times, the simulation stops. If not specified, the simulation stops when a philosopher dies.
External functs. memset, printf, malloc, free, write, usleep, gettimeofday, pthread_create, pthread_detach, pthread_join, pthread_mutex_init, pthread_mutex_destroy, pthread_mutex_lock, pthread_mutex_unlock
Libft authorized No
Description Philosophers with threads and mutexes
  • Philosopher number starts at 1 to number_of_philosophers.

  • Each philosopher should be a thread.

  • Each philosopher has one fork.

  • To eat, a philosopher needs to have two forks.

  • Prevent philosophers from duplicating forks, protect the forks state with a mutex for each of them.

  • They should avoid dying.

  • Log:

    • Log the following events:

      • timestamp_in_ms X has taken a fork
      • timestamp_in_ms X is eating
      • timestamp_in_ms X is sleeping
      • timestamp_in_ms X is thinking
      • timestamp_in_ms X died
    • X is the philosopher number.

    • The timestamp is the time in milliseconds from the start of the simulation.

    • The simulation should stop when a philosopher dies.

    • Logs messages should not mixe with each other.

    • Log message annoncing the death of a philosopher should wait no more than 10ms (after the death).

Code

Mandatory part

Forks and Philo disposition:

flowchart TB
  classDef forks fill:#2fa, color:#000;
  classDef forks_ptr fill: #2fa, color: #000, stroke: #fff, stroke-dasharray: 5, 5, stroke-width: 5px;
  classDef philo fill:#fa1, color:#000;

  subgraph Philosophers
    direction TB
    Philo1:::philo
    subgraph Philo1
      direction TB
      left_fork1:::forks_ptr
      right_fork1:::forks_ptr
    end
    Philo2:::philo
    subgraph Philo2
      direction TB
      left_fork2:::forks_ptr
      right_fork2:::forks_ptr
    end
    Philo3:::philo
    subgraph Philo3
      direction TB
      left_fork3:::forks_ptr
      right_fork3:::forks_ptr
    end
    Philo4:::philo
    subgraph Philo4
      direction TB
      left_fork4:::forks_ptr
      right_fork4:::forks_ptr
    end
  end
  subgraph Forks
    direction TB
    forks1:::forks
    forks2:::forks
    forks3:::forks
    forks4:::forks
    forks5:::forks
  end
  left_fork1 -.-> forks1
  right_fork1 -.-> forks2
  left_fork2 -.-> forks2
  right_fork2 -.-> forks3
  left_fork3 -.-> forks3
  right_fork3 -.-> forks4
  left_fork4 -.-> forks4
  right_fork4 -.-> forks5
  forks5 -.- left_fork5
  forks1 -.- right_fork5
  Philo5:::philo
  subgraph Philo5
    direction TB
    left_fork5:::forks_ptr
    right_fork5:::forks_ptr
  end
Loading

Code:

flowchart LR
  classDef data stroke:#ff0;
  classDef data_option stroke:#ff0,stroke-dasharray: 5, 5;
  classDef ressource fill:#2fa, color:#000;
  classDef process fill:#fa1, color:#000;
  classDef thread fill:#fa0, color:#000;
  classDef mutex fill:#f2a, color:#000;
  classDef protected fill:#2fa, stroke:#f2a, stroke-width:3px, color:#000;
  classDef important fill:#0af,color:#000;
  classDef eat fill:#0f0, color:#000;
  classDef think fill:#fcf, color:#000;
  classDef sleep fill:#00f, color:#fff;
  classDef death fill:#f00, color:#fff;

  Main:::important
  Main -.- |">= 5\n&&\n<= 6"| argc:::data
  Main -.- argv:::data
  argv --> Parse
  argc --> Parse
  subgraph Parse
    direction LR
    parse_is_num --> |"check char"| args_5
    parse_is_supported -->|"<= 200 "| args_5
    subgraph args_5["Argc <= 6"]
      subgraph args_4["Argc == 5"]
        time_to_die:::data
        time_to_eat:::data
        time_to_sleep:::data
        number_of_philosophers:::data
      end
      number_of_times_each_philosopher_must_eat:::data_option
    end
  end
  mutexe_print:::mutex
  mutexe_death:::mutex
  mutexe_forks:::mutex
  death:::death
  Parse --> Init --> Philosophers
  subgraph Init
    forks_array
    philos_array
    start_time
  end
  subgraph Philosophers
    subgraph Philo
      id
      mutexe_eat:::mutex
      eat_count -.- mutexe_eat
      last_meal -.- mutexe_eat
      state
      thread_philo:::thread
    end
      mutexe_fork:::mutex
    number_of_philosophers -.-> loop_philo
    loop_philo(("Loop\nphilosophers")) --> |"1 thread"| thread_philo:::thread
    loop_philo --> |"1 fork"| mutexe_fork:::mutex
  end
  Philo --> |"id % 2\nwait to separate\neven and odd id"| Philo_Life
  subgraph Philo_Life
    think:::think
    take_forks
    eat:::eat
    drop_forks
    sleep:::sleep
  end
  take_forks -.- mutexe_forks
  take_forks -.- mutexe_fork
  drop_forks -.- mutexe_forks
  eat -.- mutexe_eat
  subgraph Watcher
    direction LR
    check_death
    check_food
    check_time_to_die
  end
  check_death -.- mutexe_death:::mutex
  check_time_to_die -.- mutexe_death:::mutex
  check_time_to_die --> |"if gettime() - last_meal\n>=\ntime_to_die"|death
  check_food --> |"if count reached\nstop threads with\ndeath flag"|death
  check_food -.- mutexe_eat
  subgraph Print
    direction LR
    print_message
  end
  print_message -.- mutexe_print:::mutex
  print_message -.- check_death
Loading

Debug

Valgrind

valgrind --tool=helgrind ./program arg1 arg2 ...

The --tool=helgrind option in Valgrind is used to enable the Helgrind tool. Helgrind is a Valgrind tool for detecting synchronization errors in C, C++ and other multithreaded programs.

Here's what it can do:

  1. Detect potential data races: A data race occurs when two threads access the same memory location concurrently, and at least one of the accesses is a write.

  2. Detect misuses of the POSIX pthreads API: This includes scenarios such as unlocking a mutex that is not locked, locking a mutex that is already locked by the same thread (unless the mutex is of type PTHREAD_MUTEX_RECURSIVE), and many others.

  3. Detect deadlocks: A deadlock occurs when two or more threads cannot proceed because each is waiting for the other to release a resource.

By using valgrind --tool=helgrind your_program, you can check your multithreaded program for these types of synchronization errors.

Tips

Problem encountered with valgrind:

  • Using valgrind may cause problem with time check
    • so to debug you can increase usleep time in order for valgrind to work properly if you have a problem with time check.

Testeur

Example of Test :

Death Checks Meal Checks No Death Checks
./philo 1 800 200 200 1 ./philo 2 800 200 200 2 ./philo 3 610 200 200
./philo 4 310 200 100 2 ./philo 4 410 200 200 4 ./philo 5 800 200 200
./philo 5 300 60 600 3 ./philo 5 800 200 200 5 ./philo 200 410 200 200

Visualisation

Resources

⏯️ Unix Threads - CodeVault

📑 Wiki - Philosopher's problem 🍝

Notions

Threads

graph LR
  classDef ressource fill:#2fa, color:#000;
  classDef process fill:#fa1, color:#000;
    subgraph Process
        Ressource1[Memory]:::ressource
        point(( )) --> A[Thread 1]
        point(( )) --> B[Thread 2]
        point(( )) --> C[Thread 3]
        A -.- Ressource1
        B -.- Ressource1
        C -.- Ressource1
    end
    Process2:::process
    subgraph Process2
        Ressource3[Memory2]:::ressource
        point2(( )) --> D[Thread 1]
        point2(( )) --> E[Thread 2]
        D -.- Ressource3
        E -.- Ressource3
    end
    Process x--x |Isolated\nMemory| Process2
Loading

In computer science, a thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler, which is typically a part of the operating system. In many cases, a thread is a component of a process.

The multiple threads of a given process may be executed concurrently (via multithreading capabilities), sharing resources such as memory, while different processes do not share these resources. In particular, the threads of a process share its executable code and the values of its dynamically allocated variables and non-thread-local global variables at any given time.

The implementation of threads and processes differs between operating systems.

  • Threads offer several advantages over processes, including
    • Faster creation, termination, and context switching times due to sharing the same address space and resources.
    • Simplified communication between threads within the same process.
    • Improved resource utilization and efficiency in multithreaded applications.

Issues with threads include

  • Difficulty in writing and debugging multithreaded applications.
  • Increased complexity in managing shared resources

Race conditions

A race condition occurs when the behavior of a system depends on the relative timing of events, such as the order in which threads are scheduled. This can lead to unpredictable results and bugs that are difficult to reproduce.

Like for example, if two threads are trying to access the same shared resource, and one of them modifies the resource while the other is reading it, the result can be unpredictable.

Deadlocks

A deadlock is a situation in which two or more competing actions are each waiting for the other to finish, and thus neither ever does. Deadlocks can occur in multithreaded programs when threads acquire multiple locks in different orders, leading to a situation where each thread is waiting for a lock that is held by another thread. Example

Other infos

Data Race: This is a condition where two or more threads access shared data simultaneously and at least one of them modifies the data. This can lead to unpredictable results if not handled properly.

Condition Variables: These are synchronization primitives used in multithreaded programming. A condition variable allows one thread to signal to one or more other threads that a certain condition has changed (for example, data has been produced that the other threads can consume).

Thread Synchronization: This is a general term for techniques that ensure that threads correctly interact with each other and with shared data. This can involve mutexes, locks, semaphores, condition variables, and other techniques.

Mutexes

graph TD
  classDef ressource fill:#2fa, color:#000;
  classDef process fill:#fa1, color:#000;
  classDef thread fill:#fa0, color:#000;
  classDef mutex fill:#f2a, color:#000;
  classDef protected fill:#2fa, stroke:#f2a, stroke-width:3px, color:#000;
    subgraph Process
        Ressource1[Memory]:::protected
        Ressource2[Files]:::ressource
        Mutex1[Mutex]:::mutex o--o Ressource1
        point(( )) --> thread1[Thread 1]:::thread
        subgraph thread1
            Mutex1
        end
        thread2 -.-x Ressource1
        point(( )) --> thread2[Thread 2]:::thread
        subgraph thread2
        end
        thread3 -.-x Ressource1
        point(( )) --> thread3[Thread 3]:::thread
        subgraph thread3
        end
    end
Loading

A mutex, short for "mutually exclusive," is a synchronization primitive used to protect shared resources from being accessed simultaneously by multiple threads. It ensures that only one thread can access the protected resource at a time. When a thread acquires a mutex, other threads attempting to acquire the same mutex are blocked until the owning thread releases it. This mechanism is crucial in multi-threaded programming to prevent data races and ensure data integrity.

Mutexes differ from semaphores in their use and constraints. While both are used for synchronization, a mutex is owned by the thread that locks it and must be unlocked by the same thread. This ownership constraint helps avoid problems such as priority inversion, premature task termination, and accidental release of the mutex. Semaphores, on the other hand, are more general-purpose synchronization primitives that can be signaled by any thread and do not have an ownership requirement.

Semaphores (bonus)

Semaphores are a synchronization primitive used to control access to a shared resource by multiple threads. They are often used to solve the producer-consumer problem, where one or more threads produce data and one or more threads consume it.

A semaphore can be thought of as a counter that is used to control access to a shared resource. When a thread wants to access the resource, it must first acquire the semaphore. If the semaphore's value is greater than zero, the thread decrements the value and continues. If the value is zero, the thread is blocked until the value becomes greater than zero.

When a thread is finished with the resource, it releases the semaphore, incrementing its value. If there are any threads waiting for the semaphore, one of them is unblocked and allowed to proceed.

Functions

  • memset: Sets a block of memory with a specified value. Used in C and C++.

  • printf: Outputs formatted text to the console. Used in C and C++.

  • malloc: Allocates a block of memory and returns a pointer to it. Used in C and C++.

  • free: Deallocates a block of memory that was previously allocated by malloc, calloc, or realloc. Used in C and C++.

  • write: Writes data from a buffer into a file or a file descriptor. Used in C and C++.

  • usleep: Suspends execution of the calling thread for (at least) the number of microseconds specified. Used in Unix-like operating systems.

  • gettimeofday: Gets the current time. Used in Unix-like operating systems.

  • pthread_create: Creates a new thread. Used in POSIX threads (pthreads).

  • pthread_join: Suspends execution of the calling thread until the target thread finishes execution. Used in pthreads.

  • pthread_mutex_init: Initializes a mutex. Used in pthreads.

  • pthread_mutex_destroy: Destroys a mutex. Used in pthreads.

  • pthread_mutex_lock: Locks a mutex. Used in pthreads.

  • pthread_mutex_unlock: Unlocks a mutex. Used in pthreads.

usleep

#include <unistd.h>

int usleep(useconds_t usec);
  • Suspends execution of the calling thread for (at least) the number of microseconds specified.

Parameters:

  • usec: The number of microseconds to suspend execution.
    • useconds_t is an unsigned integer type.

Return value:

  • On success, usleep returns 0.
  • On error, it returns -1.
Example
#include <unistd.h>

int main()
{
    printf("Hello\n");
    usleep(1000000);  // Sleep for 1 second
    printf("World\n");

    return 0;
}

In this example, the program will print "Hello", wait for 1 second, and then print "World".

gettimeofday

#include <sys/time.h>

int gettimeofday(struct timeval *tv, struct timezone *tz);

timeval structure:

struct timeval
{
    time_t      tv_sec;     // Seconds
    suseconds_t tv_usec;    // Microseconds
};
  • Gets the current time.

Parameters:

  • tv: A pointer to a timeval structure that will be filled with the current time.
  • tz: A pointer to a timezone structure that will be filled with the current timezone information.
    • This parameter is generally not used and can be set to NULL.

Return value:

  • On success, gettimeofday returns 0.
  • On error, it returns -1.

Info:

The tv_sec field represents the number of seconds elapsed since the Unix Epoch (00:00:00 UTC, January 1, 1970), and the tv_usec field represents the number of microseconds in the current second.

Example
#include <stdio.h>
#include <sys/time.h>

int main() {
    struct timeval tv;

    gettimeofday(&tv, NULL);

    printf("Seconds: %ld\n", tv.tv_sec);
    printf("Microseconds: %ld\n", tv.tv_usec);

    return 0;
}

In this example, the program will print the current time in seconds and microseconds.

Pthreads functions

pthread_create

Compile and link with -pthread.

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
  • Creates a new thread.

Parameters:

  • thread: A pointer to a pthread_t structure that will be filled with the thread ID.
  • attr: A pointer to a pthread_attr_t structure that specifies the thread's attributes.
    • This parameter is generally not used and can be set to NULL.
  • start_routine: A pointer to the function that will be executed by the new thread.
  • arg: A pointer to the argument that will be passed to the start_routine function.

Return value:

  • On success, pthread_create returns 0.
  • On error, it returns a positive error number.

Info:

  • You have to check the return value of pthread_create to ensure that the thread was created successfully.
Example

Simple example:

#include <stdio.h>
#include <pthread.h>

void *print_message(void *ptr) {
    char *message = (char *)ptr;
    printf("%s\n", message);
    sleep(1);
    return NULL;
}

int main() {
    pthread_t thread;
    char *message = "Hello";

    pthread_create(&thread, NULL, print_message, (void *)message);

    pthread_join(thread, NULL);

    printf(", world!\n");

    return 0;
}

In this example, the program creates a new thread that prints "Hello, world!".

Multiple threads example:

#include <stdio.h>
#include <pthread.h>

void* routine()
{
	printf("Hello from thread\n");
	sleep(5);
	printf("Goodbye from thread\n");
	return NULL;
}

int main() {
  pthread_t thread;
	pthread_t thread2;

	pthread_create(&thread, NULL, &routine, NULL);
	pthread_create(&thread2, NULL, &routine, NULL);

	printf("Hello from main\n");
	sleep(6);
	printf("Goodbye from main\n");
	pthread_join(thread, NULL);
	pthread_join(thread2, NULL);
	return 0;
}

In this example, the program creates two new threads that print "Hello from thread" and "Goodbye from thread" after 5 seconds. The main thread prints "Hello from main" and "Goodbye from main" after 6 seconds.

pthread_join

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
  • Suspends execution of the calling thread until the target thread finishes execution.

Parameters:

  • thread: The thread ID of the target thread.
  • retval: A pointer to a pointer that will be filled with the return value of the target thread.
    • This parameter is generally not used and can be set to NULL.

Return value:

  • On success, pthread_join returns 0.
  • On error, it returns a positive error number.

Mutexes functions

Example/Demo
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex;

void *print_message(void *ptr) {
    pthread_mutex_lock(&mutex);
    char *message = (char *)ptr;
    printf("%s\n", message);
    sleep(1);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t thread;
    char *message = "Hello";

    pthread_mutex_init(&mutex, NULL);

    pthread_create(&thread, NULL, print_message, (void *)message);

    pthread_join(thread, NULL);

    printf(", world!\n");

    pthread_mutex_destroy(&mutex);

    return 0;
}

In this example, the program creates a new thread that prints "Hello, world!".

pthread_mutex_init

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • Initializes a mutex.

Parameters:

  • mutex: A pointer to a pthread_mutex_t structure that will be initialized.
  • attr: A pointer to a pthread_mutexattr_t structure that specifies the mutex's attributes.
    • This parameter is generally not used and can be set to NULL.

Structure:

The actual content of the mutex structure is not defined in the POSIX threads (pthreads) standard, and it is generally considered to be opaque -- that is, you should not access its members directly. Instead, you should use the pthreads API functions to work with mutexes.

Remember, you should always use the pthreads API functions to work with pthread_mutex_t objects, and not try to manipulate them directly.

Return value:

  • On success, pthread_mutex_init returns 0.
  • On error, it returns a positive error number.

Info:

  • You have to check the return value of pthread_mutex_init to ensure that the mutex was initialized successfully.
  • If the system does not have enough resources (like memory) to initialize a new mutex, pthread_mutex_init will fail.

pthread_mutex_destroy

#include <pthread.h>

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • Destroys a mutex.

Parameters:

  • mutex: A pointer to a pthread_mutex_t structure that will be destroyed.

Return value:

  • On success, pthread_mutex_destroy returns 0.
  • On error, it returns a positive error number.
Error Code Description
EAGAIN The system lacked the necessary resources (other than memory) to initialize another mutex.
ENOMEM Insufficient memory exists to initialize the mutex.
EPERM The caller does not have the privilege to perform the operation.
EINVAL The attributes object referenced by attr has the robust mutex attribute set without the process-shared attribute being set.

Info:

  • You have to check the return value of pthread_mutex_destroy to ensure that the mutex was destroyed successfully.
  • If a thread attempts to destroy a mutex that is currently locked, it will cause an error.

pthread_mutex_lock

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
  • Locks a mutex.

Parameters:

  • mutex: A pointer to a pthread_mutex_t structure that will be locked.

Return value:

  • On success, pthread_mutex_lock returns 0.
  • On error, it returns a positive error number.
Error Code Description
EAGAIN The maximum number of recursive locks for the mutex has been exceeded.
EINVAL The mutex was created with the protocol attribute having the value PTHREAD_PRIO_PROTECT and the calling thread's priority is higher than the mutex's current priority ceiling.
ENOTRECOVERABLE The state protected by the mutex is not recoverable.
EOWNERDEAD The mutex is a robust mutex and the process containing the previous owning thread terminated while holding the mutex lock. The mutex lock shall be acquired by the calling thread and it is up to the new owner to make the state consistent.
EDEADLK The mutex type is PTHREAD_MUTEX_ERRORCHECK and the current thread already owns the mutex, or a deadlock condition was detected.
EBUSY The mutex could not be acquired because it was already locked.
EPERM The mutex type is PTHREAD_MUTEX_ERRORCHECK or PTHREAD_MUTEX_RECURSIVE, or the mutex is a robust mutex, and the current thread does not own the mutex.

Info:

  • If the mutex is already locked by another thread, the calling thread will be blocked until the mutex is unlocked by the other thread.

  • The difference between pthread_mutex_lock and pthread_mutex_trylock is that pthread_mutex_lock will block the calling thread until the mutex is unlocked, while pthread_mutex_trylock will return an error if the mutex is already locked.

  • You have to check the return value of pthread_mutex_lock to ensure that the mutex was locked successfully.

pthread_mutex_unlock

#include <pthread.h>

int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • Unlocks a mutex.

Parameters:

  • mutex: A pointer to a pthread_mutex_t structure that will be unlocked.

Return value:

  • On success, pthread_mutex_unlock returns 0.
  • On error, it returns a positive error number.

Demo threads and mutexes functions

🧪 Examples/Demos - CodeVault

Demo of create threads in loops:

In this example, the program creates 4 threads that increment the mails variable 1,000,000 times each. The mails variable is protected by a mutex.

#include <stdio.h>
// THREAD
#include <pthread.h>

int mails = 0;
pthread_mutex_t mutex;

void* routine()
{
	int i = 0;
	while (i < 1000000)
	{
		i++;
		pthread_mutex_lock(&mutex); // Lock the mutex for current thread
		mails++;
		pthread_mutex_unlock(&mutex); // Unlock the mutex for current thread
	}
	return (NULL);
}

int main() {
    pthread_t th[4];

	pthread_mutex_init(&mutex, NULL); // Initialize the mutex protection

	printf("Mails: %d\n", mails);
	for (int i = 0; i < 4; i++)
	{
		if (pthread_create(&th[i], NULL, routine, NULL) != 0)
		{
			printf("Failed to create thread %i\n", i);
			return 1;
		}
		printf("Thread %i has started\n", i);
	}
	for (int i = 0; i < 4; i++)
	{
		if (pthread_join(th[i], NULL) != 0)
		{
			printf("Failed to join thread %i\n", i);
			return 1;
		}
		printf("Thread %i has finished\n", i);
	}
	pthread_mutex_destroy(&mutex); // Destroy the mutex protection

	printf("Mails: %d\n", mails); // 2000000 but if we don't use mutex, it will be less than 2000000
	return 0;
}

Demo recover return value from threads:

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <time.h>

void	*roll_dice()
{
	
	int value = (rand() % 6) + 1;
	int *ptr_res = malloc(sizeof(int));
	printf("You rolled a %d\n", value);
	*ptr_res = value;
	printf("ptr_res: %p\n", ptr_res);
	return (void *)ptr_res;
}

int main()
{
	srand(time(NULL));
	pthread_t 	th;
	int *res;

	// one thread
	printf("================= One Thread ===================\n");
	pthread_create(&th, NULL, roll_dice, NULL);
	pthread_join(th, (void **)&res);
	printf("res roll: %d\n", *res);
	printf("res addr: %p\n", res);
	free(res);

	// Multiple threads
	printf("================= Multiple Threads ===================\n");
	pthread_t 	th_list[4];
	int 		*res_list[4];
	for (int i = 0; i < 4; i++)
	{
		pthread_create(&th_list[i], NULL, roll_dice, NULL);
	}
	for (int i = 0; i < 4; i++)
	{
		pthread_join(th_list[i], (void **)&res_list[i]);
	}
	for (int i = 0; i < 4; i++)
	{
		printf("res_list[%d] roll: %d\n", i, *res_list[i]);
		printf("res_list[%d] addr: %p\n", i, res_list[i]);
		free(res_list[i]);
	}
	return 0;
}

Demo of create threads with arguments:

In this example, the program creates 10 threads that print the prime numbers from an array. The threads receive the index of the prime number as an argument.

  • The first example uses the stack to pass the argument to the thread.

  • The second example uses the heap to pass the argument to the thread.

The second example is better because the stack is not safe for threads. The stack is shared between threads, and the value of the argument can change before the thread uses it.

In this example, the value of i is send as an address, so the value of i can change before the thread uses it.

// Standard Libs
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Time
#include <sys/time.h>
// Threads
#include <pthread.h>

int primes[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};

void* routine(void *arg)
{
	int index = *(int*)arg;
	printf("Prime: %d\n", primes[index]);
	return (NULL);
}

void* routine2(void *arg)
{
	int index = *(int*)arg;
	printf("Prime: %d\n", primes[index]);
	free(arg);
	return (NULL);
}

int main()
{
	pthread_t thread[10];

	// Example of arg from the stack
	printf("=================== Example Stack Arg ===================\n");
	for (int i = 0; i < 10; i++)
	{
		if (pthread_create(&thread[i], NULL, &routine, &i) != 0)
		{
			perror("Failed to pthread_create");
		}
	}
	for (int i = 0; i < 10; i++)
	{
		if (pthread_join(thread[i], NULL) != 0)
		{
			perror("Failed to pthread_join");
		}
	}
	
	// Example of arg from the heap
	printf("=================== Example Heap Arg ===================\n");
	for (int i = 0; i < 10; i++)
	{
		int *index = malloc(sizeof(int));
		*index = i;
		if (pthread_create(&thread[i], NULL, &routine2, index) != 0)
		{
			perror("Failed to pthread_create");
		}
	}
	for (int i = 0; i < 10; i++)
	{
		if (pthread_join(thread[i], NULL) != 0)
		{
			perror("Failed to pthread_join");
		}
	}
	return (0);
}

Demo summing from array with threads:

In this example, the program creates 2 threads that sum the prime numbers from an array. The threads receive the index of the prime number as an argument and return the sum of the prime numbers.

// Standard Libs
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Time
#include <sys/time.h>
// Threads
#include <pthread.h>

int primes[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};

void* routine(void *arg)
{
	int index = *(int*)arg;
	int sum = 0;
	for (int j = 0; j < 5; j++)
	{
		sum += primes[index + j];
	}
	*(int*)arg = sum;
	return (arg);
}

int main()
{
	pthread_t thread[2];

	int i;
	for (i = 0; i < 2; i++)
	{
		int *index = malloc(sizeof(int));
		*index = i * 5;
		if (pthread_create(&thread[i], NULL, &routine, index) != 0)
		{
			perror("pthread_create");
		}
	}
	int main_sum = 0;
	for (i = 0; i < 2; i++)
	{
		int *result;
		if (pthread_join(thread[i], (void **)&result) != 0)
		{
			perror("pthread_join");
		}
		main_sum += *result;
		printf("Thread %d returned %d\n", i, *result);
		free(result);
	}
	printf("Sum of all primes is %d\n", main_sum);
	return (0);
}

Demo deadlock with threads:

In this example, the program creates 8 threads that try to lock two mutexes. The threads lock the mutexes in different orders, which causes a deadlock.

// Standard Libs
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Time
#include <sys/time.h>
// Threads
#include <pthread.h>

#define THREAD_COUNT 8

pthread_mutex_t mutexFuel;
pthread_mutex_t mutexCargo;

int fuel = 50;

// The Mutexes lock order is important
// If the order is not the same, the program can/will deadlock
void* routine(void *arg)
{
	(void)arg;
	if (rand() % 2)
	{
		pthread_mutex_lock(&mutexFuel);
		sleep(1);
		pthread_mutex_lock(&mutexCargo);
	}
	else
	{
		pthread_mutex_lock(&mutexCargo);
		sleep(1);
		pthread_mutex_lock(&mutexFuel);
	}
	fuel += 50;
	printf("Increased Fuel to: %d\n", fuel);
	pthread_mutex_unlock(&mutexFuel);
	pthread_mutex_unlock(&mutexCargo);
	return (NULL);
}

int main()
{
	pthread_t thread[THREAD_COUNT];

  pthread_mutex_init(&mutexFuel, NULL);
  pthread_mutex_init(&mutexCargo, NULL);
	int i;
	for (i = 0; i < THREAD_COUNT; i++)
	{
		if (pthread_create(&thread[i], NULL, routine, NULL) != 0)
		{
			printf("Error creating thread\n");
			return (1);
		}
	}
	for (i = 0; i < THREAD_COUNT; i++)
	{
		if (pthread_join(thread[i], NULL) != 0)
		{
			printf("Error joining thread\n");
			return (1);
		}
	}
	printf("Fuel: %d\n", fuel);
	pthread_mutex_destroy(&mutexFuel);
  pthread_mutex_destroy(&mutexCargo);
	return (0);
}

Demo barrier with threads (not in the subject):

Barrier infos

A barrier is a thread synchronization mechanism in POSIX threads (pthreads) where all threads are blocked until all participating threads reach the barrier. Once all threads have reached the barrier, they are all released and can continue executing.

Here's an example of how to use a barrier in pthreads:

#include <pthread.h>
#include <stdio.h>

#define NUM_THREADS 5

pthread_barrier_t barrier;

void* threadFunc(void* id) {
    int thread_id = *(int*)id;

    printf("Before barrier: %d\n", thread_id);
    pthread_barrier_wait(&barrier);
    printf("After barrier: %d\n", thread_id);

    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];
    int thread_ids[NUM_THREADS];

    // Initialize the barrier and set it to NUM_THREADS
    pthread_barrier_init(&barrier, NULL, NUM_THREADS);

    // Start up the threads
    for (int i = 0; i < NUM_THREADS; i++) {
        thread_ids[i] = i;
        pthread_create(&threads[i], NULL, threadFunc, &thread_ids[i]);
    }

    // Wait for all threads to finish
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    // Destroy the barrier
    pthread_barrier_destroy(&barrier);

    return 0;
}

In this example, pthread_barrier_init initializes the barrier. The second argument can be used to specify attributes for the barrier (NULL means default attributes), and the third argument is the count of the number of threads that must call pthread_barrier_wait before any of them successfully return from the call.

The pthread_barrier_wait function is used to indicate that the thread is at the barrier. Each thread will block until all threads have reached the barrier.

Finally, pthread_barrier_destroy is used to free a barrier when you're done with it.