Skip to content

1. Concurrency I

26. Introduction to Concurrency

  • Threads are lightweight processes, that share the same address space; thus they share the same heap but each thread has its own stack.
  • In a single processor, multithreaded process; switching between threads requires context-switching, to unload the previous TCB and load the new TCB (Thread Control Block) into the CPU; however, this is different from process context-switching that it does not require page switch, since the two threads share the same address space.
  • Why do you need threads:
    1. Parallelism: threads work in parallel, as each run on a different core (CPU).
    2. Avoid blocking by I/O: if one thread is blocked by I/O, the other threads can continue to run instead of suspending the whole process until the I/O is done.
  • Creating a thread is like making a function call, but instead of executing the function first and then returning the result to the caller; a new execution thread will be created where it runs independently from the caller, and the schedular decides when to run it.
  • Critical section. a piece of code that accesses a shared resource, and must be executed by only one thread at a time (no concurrent executions are allowed).
  • Mutual exclusion. a property that ensures that only one thread can execute a critical section at a time, others must be locked.
  • Atomicity. is a property for a group of instructions that must be executed as a single unit, without interruption; the hardware guarantee that these instructions are executed fully, or not at all. Similar to the transaction in a database.
  • Synchronization primitives. are the tools that are used to implement mutual exclusion and atomicity. With the help of hardware and OS, we can implement multithreaded code that accesses critical sections in a synchronized and controlled manner.

27. Thread API

  • Thread creation (POSIX compliant) pthread create()
#include <pthread.h>
int
pthread_create(
    pthread_t *thread, // 1. pointer to a thread (type pthread_t), this is the thread location within the process
    const pthread_attr_t *attr, // 2. thread attributes, like stack size, scheduling priority, call `pthread_attr_init()` to initialize a new attribute
    void *(*start_routine)(void*), // 3. a function reference (pointer) that will be executed by the thread; entry point of the thread
    void *arg // 4. argument to be passes to 3., must match the function signature of 3.
);
  • Waiting for a thread to finish pthread join((pthread_t thread, void **value_ptr), where the second argument is a pointer to a pointer that will hold the return value of the thread.

Locks

  • There are functions provided by POSIX that are used to provide mutual exclusion for critical sections; these are called locks.
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • can be used as :
thread_mutex_t lock;
pthread_mutex_lock(&lock);
x = x + 1; // or whatever your critical section is
pthread_mutex_unlock(&lock);
  • When the lock is called:
    • if another thread is holding the lock, the current thread will be blocked until the lock is released.
    • after the lock is released, the current thread will acquire the lock and continue to execute the critical section.

Condition Variables

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
  • can be used as:
// thread 1

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
Pthread_mutex_lock(&lock);
while (ready == 0) {
    Pthread_cond_wait(&cond, &lock);
}
Pthread_mutex_unlock(&lock);


// thread 2
Pthread_mutex_lock(&lock);
ready = 1;
Pthread_cond_signal(&cond);
Pthread_mutex_unlock(&lock);
  • Notes:
    • Keep it simple (KISS).
    • Minimize thread interactions (keep your threads as independent as possible).
    • Initialize locks and conditions properly.
    • Check return values (address abnormal conditions).
    • Pass arguments carefully (avoid passing pointers allocated on the stack of the current thread).
    • Variables belonging to a thread are private to that thread and their life ends with the thread (allocate variables that you want to keep on the heap).
    • Avoid using flags (booleans) to communicate between threads (use condition variables and signals instead).

References