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 process. It 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.
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.
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))
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.
-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
(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);
[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