Tuesday, October 16, 2018

Pthread - Lets understand threads

In this blog, I have explained some basic details about threads in Linux. It explains the doubts we have while working on threads, like:

  • What is a thread?
  • What are the per-thread attributes and why it is needed as per thread attributes?
  • Why a thread works like a process
  • What happens when a multi-threaded program runs on single core or multicore?
  • How to debug a multithreaded program?
And in extra bytes, some more information which is good to know while working on threads.

What is a thread?

A thread is a lightweight process within the scope of a processIt means, for a process, you can create multiple threads. Each thread can function same as a process, however, with much lesser overheads.

As there can be multiple threads in a process, it provided a way to support parallel processing for a process. There will be multiple units of work which can run in parallel or in a synchronous way depending on the requirement.

The Producer-consumer problem is a good example of synchronous communication between threads. In producer-consumer problem, once a producer has produced items then the consumer can consume it. Depending on the number of items and other constraints, it can be a combination of parallel processing and synchronization between producer and consumer threads.



The threads of a process share process address space, open files and global memory (data and heap segments). It provides some benefits over multiple processes:

  • Creating a thread is less expensive as compared to processes as each thread share same address space.
  • Context switch between threads(scheduling different threads to run) is faster as compared to processes. 
  • Sharing data between threads is much faster, as the data produced by one thread is immediately available to other threads.



Why thread works like a process?


In Linux, a thread is created using system call clone(2). Clone system call creates a new process; however, the newly created process shares some parts of the calling process.

So, in a way, a thread is a process, which shares some parts with its parent process.

The clone system call is used with different parameters to allow it to share virtual address space, open files and signals. Use “man 2 clone” for more information on this.
DESCRIPTION
       clone() creates a new process, in a manner similar to fork(2)
       .…

       Unlike  fork(2),  clone()  allows  the  child  process to share
       parts of its execution context with the calling process, such as
       the virtual address space, the table of file descriptors, and 
       the table  of  signal  handlers. (Note  that  on  this  manual
       page,  "calling  process" normally corresponds to "parent 
       process".  But see the description of CLONE_PARENT below.)

       One use of clone() is to implement threads: multiple flows of 
       control in a program that run concurrently in a shared address 
       space.

What happens when a multithreaded program runs on a single core or multicore systems?

Explaining here the basic concept need to understand for multithreading. 

Single core system

In a single core system, only one thread can run at a time. However, by slicing the time, it gives an impression that multiple threads are running concurrently.

So, in a single core system, multiple threads do not give any benefit on time and efficiency. The reason is,  we are running each thread sequentially one by one depending on scheduling algorithm used. Also adding an overhead of context switch between threads.

However, the time slice for each thread is very small. For end user, it appear as if multiple works are happening in parallel.

Multicore system

In a multicore system, multiple threads can run concurrently and result in parallel processing. More than one thread is scheduled and run at the same time. A multicore system actually gives the benefit of multithreading on time and efficiency.



Per Thread attributes


The threads execute in a similar manner as a process. To achieve this, each thread has few process attributes like the stack, program counter, and registers of its own.


  • Stack: Stack is needed for local variables. Same as a process, each thread has its own stack. It is needed for independent execution of threads.
  • Program counter and set of Registers: Program counter keeps the address of the next instruction to be executed. Each thread has its own program counter and registers, allow threads to execute instructions at runtime independent of the process.
  • Thread ID: Each thread is represented by a unique thread ID. Needed to identify the thread.
  • Signal Mask: Each thread has its own signal mask. This helps to deliver a signal to a particular thread. The important point is, all the threads share signal disposition. It means, signals are sent to a process(not thread) and it can be received by any thread. By masking a signal for other threads, we can design to receive signal by a particular thread.
  • Each thread has its own errno variable. If all the threads share the same errno, then an error in one thread will affect the functioning of other threads.
  • Alternate signal stack (sigaltstack(2))
  • Real-time scheduling policy and priority (sched(7))
Note: One important per-thread attribute is thread ID. Each thread is assigned a unique ID. It is assigned at the time of thread creation. This thread ID is used by other thread function to identify the thread.

Process attributes which are shared by all threads


Rest of the parameters are shared by all threads. It includes code segment, global memory(data and heap segments), open files, signal disposition. All threads have the same process ID, parent process ID, process group ID, user and group ID, current working directory etc.

Debugging a simple program


We will use a simple example to debug using GDB. Will check the thread information, switch between different threads and check thread's local variables.


  1 #include <stdio.h>
  2 #include <pthread.h>
  3 #include <errno.h>
  4 
  5 void *simpleFunction(void *);
  6 
  7 int main()
  8 {
  9         int rc, m  = 25;
 10         pthread_t th;
 11 
 12         printf("main: Creating thread\n");
 13         if(rc = pthread_create(&th, NULL, &simpleFunction, NULL))
 14         {
 15                 printf("Thread creation failed, return code %d, errno %d", rc, errno);
 16         }
 17         printf("main: After creating thread. Variable m = %dn", m);
 18 
 19         pthread_join(th, NULL);
 20         return 0;
 21 }
 22 
 23 void *simpleFunction(void *)
 24 {
 25         int i = 5, j = 10, k = 15;
 26         printf("simpleFunction: In thread start function\n");
 27         printf("Value of i = %d, j = %d, k = %d\n", i, j, k);
 28         return NULL;
 29  }



Compile and run

Compile the above program with "-g" and "-pthread" options.

g++ -g -pthread simple.c
Always use “-pthread” while compiling a program with threads. It not only compiles and links with the correct version of pthread library also adds any flags needed for pthread library.

-g option is used to debug using GDB.

Check threads with "ps" command and GDB

ps output shows threads created by the above program. 

root@xxxx-VirtualBox:~/pthread_tst/blog# ps -elfT |grep a.out
F S UID        PID  SPID  PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
0 S root     13760 13760  2899  0  80   0 - 26941 poll_s 15:13 pts/1    00:00:00 gdb -q ./a.out
0 t root     13772 13772 13760  0  80   0 -  3722 ptrace 15:14 pts/1    00:00:00 ./a.out
1 t root     13772 13776 13760  0  80   0 -  3722 ptrace 15:14 pts/1    00:00:00 ./a.out


Two threads are running. The first column is Id, which shows the ID given by GDB. This is not thread ID.

Next to Thread is thread ID. For main thread its "0x7ffff7fdf740". The "*" indicates the current thread running. It shows the current instruction being executed by each thread.

(gdb) info threads
  Id   Target Id         Frame 
* 1    Thread 0x7ffff7fdf740 (LWP 13772) "a.out" main () at simple.c:17
  2    Thread 0x7ffff77c4700 (LWP 13776) "a.out" simpleFunction () at simple.c:25


Use the command "thread <Id>" to switch to the thread Id shown in the first column in the above output. 



(gdb) thread 1
[Switching to thread 1 (Thread 0x7ffff7fdf740 (LWP 13772))]
#0  main () at simple.c:17
17  printf("main: After creating thread. Variable m = %dn", m);

Some more output


[Switching to Thread 0x7ffff77c4700 (LWP 13776)]

Thread 2 "a.out" hit Breakpoint 2, simpleFunction () at simple.c:25
25  int i = 5, j = 10, k = 15;
(gdb) n
26  printf("simpleFunction: In thread start function\n");

# After few instuctions
(gdb) thread 1
[Switching to thread 1 (Thread 0x7ffff7fdf740 (LWP 13772))]
#0  0x00007ffff7bbed2d in __GI___pthread_timedjoin_ex (threadid=140737345505024, thread_return=0x0, abstime=0x0, 
    block=) at pthread_join_common.c:89
89 pthread_join_common.c: No such file or directory.
(gdb) thread 2
[Switching to thread 2 (Thread 0x7ffff77c4700 (LWP 13776))]
#0  simpleFunction () at simple.c:28
28  return NULL;



Extra bytes

  • pthread functions return value: Most thread functions return 0 on SUCCESS and return error number on failure. Note that thread functions do not set errno variable.
  • Each thread is a schedulable entity. In Linux, each thread is assigned a task structure. The task structure is a schedulable entity and gets its time quantum to be scheduled
  • Thread safe function: A thread-safe function is a function that can safely be called from multiple threads at the same time. When called from multiple threads at the same time, its result should not be affected.
  • Cancellation point: POSIX.1 specifies some functions as cancellation points. If a thread is canceled, its cancellation is deferred till it calls a function which is declared as cancellation point.
  • Signal disposition: For each signal number the disposition is set, which explains the action needs to take when a signal is delivered. It is called signal disposition. The signal disposition can either be SIG_IGN(Ignore signal), SIG_DFL(Take default action) or a programmer-defined function (a "signal handler"). All threads share signal disposition.

No comments:

Post a Comment