I am reviewing threads, as I work on my assignment, which builds multi-threaded server with I/O multiplexing.

Briefly about threads

Threads are often referred to as light-weight processes, because they are literally lighter than processes. What’s lighter? The size of a thread instance (less memory) and therefore, saving creation and destruction time.

They are often preferred over processes, when one needs to run large number of tasks and creating that many number of processes takes too much memory and time.

There are different use cases for threads and processes, so use them appropriately by needs.

For instance, chrome tabs are implemented as individual processes, because then if one tab is killed, the others will still remain safe.

Processes

  • Each process is independent (No sharing of memory between processes)
  • Communication between processes can be more difficult

  • Useful when security is an important issue to avoid sharing of memory.

Threads

  • Threads are executed within processes (Every process has at least one thread : the main thread)

  • Share virtual address space, system memory along with other threads in the same process
  • Has its own stack and registers

pthread_create()

Obviously, we need to create a thread in order to use one. This call will create a thread from the main thread, which is a default thread that comes along with when a process starts.

pthread_join ()

Let’s say we run multiple threads and one of the threads take much much longer than the others. If we don’t call join, the main thread may exit before all the threads are done with their job. This will create zombie threads. Therefore, if it is necessary to wait for all the threads to finish their tasks, it is important to call join to guarantee that all threads are done before the program exits.

pthread_mutex_lock(), pthread_mutex_unlock()

As mentioned in the intro, threads share memory. They can read and write to the same data. If each task assigned to threads modifies shared data, this will result in unexpected error from race condition. Race condition happens when threads access and change the same data almost at the same instant, so they end up reading and modifying the wrong data. Therefore, to prevent this from happening, it is important to make reading and writing operations atomic, so that when one thread is accessing and doing whatever it wants with the data, no other threads can access that same data.

pthread_cond_signal(), pthread_cond_wait()

Condition variable is needed to improve cpu usage by keeping threads in waiting status until they receive some external signal, rather than making the thread constantly check for notification.

Thread pool

Creating thousands of threads for each job can be quite inefficient, especially in terms of memory. In this case, using thread pool would be a better choice. Thread pool makes use of limited number of threads by assigning multiple jobs to each thread. For instance, to execute 10,000 jobs, instead of creating 10,000 threads, we can create 100 threads and let each of them perform 100 jobs each.

  1. Create thread pool (ex. 10 threads). For each thread in the thread pool, it will constantly watch for a new task to be assigned.
  2. When a new task is created, put it into a queue.
  3. When a thread receives a new task, it will dequeue and handle the task.

Use condition variable to improve the performance, so that each thread does not constantly check if a client arrived or not.

Simple example of a server using thread pool.

It recieves many client requests (100s, 1000s …) and it distributes the client requests into 10 threads.

(This is not a full code, refer to the link in the reference section for the full code.)

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

#define SERVERPORT 22000
#define THREAD_POOL_SIZE 10 

typedef struct sockaddr_in SA_IN;
typedef struct sockaddr SA;

pthread_t thread_pool[THREAD_POOL_SIZE];
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_var = PTHREAD_COND_INITIALIZER;

void * thread_func(void* arg);
void * handle_connection(void * pclient);

int main(){
  // Create threads 
  for (int i=0; <THREAD_POOL_SIZE; i++){
    pthread_create(&thread_pool[i], NULL, thread_func, NULL);
  }
  
  // Call socket(), bind(), listen() to open up the server socket 
  server_socket = Open_listenfd(SERVERPORT);
  
  // Don't end the connection until the server explicitly gets turned off.
  while(1){
    printf("Waiting for connections...\n");
    addr_size = sizeof(SA_IN);
    
    // When a client sends connect request, accept it 
    client_socket = Accept (server_socket, (SA*) &client_addr, (socklen_t *)&addr_size);
    
    /** THREAD POOL PART **/
    // Insert the new incoming client into the queue 
    int *pclient = malloc(sizeof(int)); // Using int pointer to meet the pthread_create function form.
    *pclient = client_socket; 
    // Call lock when inserting the new client into the queue. 
    pthread_mutex_lock(&mutex);
    enqueue(pclient);
    // Signal the thread that a new client came. 
    pthread_cond_signal(&cond_var);
    pthread_mutex_unlock(&mutex);
  }
	return 0;
}

void * thread_func(void * arg){
  /* This function is run in each thread */
  
  // Just continuously run without returning 
  while (1) {
    int * pclient;
    pthread_mutex_lock(&mutex);
    // Without condvar, this part will not exist, and the thread will constantly check if there is a client in the queue.
    if ((pclient = dequeue()) == NULL){
      // If there is no client, wait until a signal arrives as a new client arrives.
      pthread_cond_wait(&cond_var, &mutex);
      pclient = dequeue();
    }
    pthread_mutex_unlock(&mutex);
    
    // If there is a client to handle, handle the client 
    if (pclient != NULL){
      handle_connection(pclient);
    }
  }
}

void * handle_connection(void * pclient){
  /* Read client's message from client socket, 
  do whatever server needs to do to process the request  from the client, 
  return the result back to the client,
  and close the current socket. 
  */
}

Parallelism vs concurrency

Parallelism : Executing multiple tasks at the same time.

Ex) Multi-processing in multi-core machine

Screen Shot 2020-12-15 at 4.12.38 PM

Concurrency : Only one task can be executed at a time, but cpu utilization is maximized by selecting and running multiple tasks.

Ex) Mult-tasking in single-core machine through time slicing.

Screen Shot 2020-12-15 at 4.12.43 PM

Reference

  • [Programming with Threads by Jacob Sorber](https://www.youtube.com/playlist?list=PL9IEJIKnBJjFZxuqyJ9JqVYmuFZHr7CFM)