Getting started with Linux system programming -- threads

Posted by gooman on Sat, 12 Feb 2022 11:40:50 +0100

1, Thread

1. Thread overview (differences from processes and advantages of threads)

Multithreading development has been supported by mature pthread library on Linux platform. The most basic concepts of multithreading development include three points: thread, mutex and condition. Among them, thread operation is divided into thread creation, exit and waiting. Mutex includes four operations: create, destroy, lock and unlock. There are five kinds of conditional operations: create, destroy, trigger, broadcast and wait. Other thread extension concepts, such as semaphores, can be encapsulated through the basic operations of the above three basic elements. See table for details:

The difference between thread and process:
A typical UNIX/Linux process can be seen as having only one control thread: a process does only one thing at a time. With multiple control threads, the process can be designed to do more than one thing at the same time, and each thread handles independent tasks.

A process is an instance of program execution. It is the basic unit for allocating system resources (CPU time, memory, etc.). In the system of thread oriented design, the process itself is not the basic running unit, but the container of threads. The program itself is only the description of instructions, data and their organizational form, and the process is the real running instance of the program (those instructions and data).

Thread is the smallest unit that the operating system can schedule operations. It is included in the process and is the actual operation unit in the process. A thread refers to a single sequential control flow in a process. Multiple threads can be concurrent in a process, and each thread executes different tasks in parallel. The thread contains the necessary information to represent the execution environment in the process, including the thread ID representing the thread in the process, a set of register values, stack, scheduling priority and policy, signal mask word, errno constant and thread private data. All information of the process is shared by all threads of the process, including executable program text, global memory and heap memory of the program, stack and file descriptor. In Unix and Unix like operating systems, threads are also called lightweight processes, but lightweight processes refer more to kernel thread s and user thread s to threads.

"Process - the smallest unit of resource allocation, thread - the smallest unit of program execution"

Processes have independent address space. After a process crashes, it will not affect other processes in protected mode, and threads are just different execution paths in a process. Threads have their own stack and local variables, but threads do not have a separate address space. The death of a thread is equal to the death of the whole process. Therefore, multi-process programs are more robust than multi-threaded programs, but they consume more resources and are less efficient when switching processes.
  
Advantages of threads:
The difference between process and thread is the reason why we use thread. Generally speaking, a process has an independent address space, and a thread does not have a separate address space (threads in the same process share the address space of the process).

One of the reasons for using multithreading is that it is a very * * frugal * * multitasking operation compared with processes. We know that under the Linux system, starting a new process must be allocated to its independent address space, and many data tables must be established to maintain its code segments, stack segments and data segments, which is an "expensive" multitasking mode of work. Multiple threads running in a process use the same address space and share most of the data with each other. The space spent on starting a thread is far less than that spent on starting a process. Moreover, the time required for switching between threads is far less than that required for switching between processes. According to statistics, generally speaking, the cost of a process is about 30 times that of a thread. Of course, this data may be quite different in specific systems.

The second reason for using multithreading is the convenient communication mechanism between threads. For different processes, they have independent data space. Data transmission can only be carried out through communication. This method is not only time-consuming, but also very inconvenient. Threads are not. Because threads in the same process share data space, the data of one thread can be directly used by other threads, which is not only fast, but also convenient. Of course, data sharing also brings some other problems. Some variables cannot be modified by two threads at the same time, and the data declared as static in some subroutines is more likely to bring a disastrous blow to multithreaded programs. These are the most important points to pay attention to when writing multithreaded programs.

In addition to the above advantages, compared with processes, multithreaded programs, as a multitasking and concurrent working mode, certainly have the following advantages:

  • Improve application response. This is especially meaningful for the program of graphical interface. When an operation takes a long time, the whole system will wait for the operation. At this time, the program will not respond to the operation of keyboard, mouse and menu, but will take a long time to use multithreading technology
    This embarrassing situation can be avoided by placing consumption) in a new thread.
  • Make the multi CPU system more efficient. The operating system will ensure that different threads run on different CPUs when the number of threads is not greater than the number of CPUs.
  • Improve program structure. A long and complex process can be divided into multiple threads and become several independent or semi independent running parts. Such a program will be conducive to understanding and modification.

2. Thread creation wait and exit

Thread creation function:

  • When pthread_ When create returns successfully, thread D of the newly created thread will be set to the memory unit pointed to by tidp. The attr parameter is used to customize various thread attributes. Now let's set it to NULL and create a thread with default properties.
  • The newly created thread starts from start_ The address of the RTN function starts running, and the function has only one typeless pointer parameter arg. If you need to start_ If the RTN function passes more than one parameter, you need to put these parameters into a structure, and then pass the address of this structure as Arg parameter.
  • When creating a thread, there is no guarantee that which thread will run first: the newly created thread or the calling thread. The newly created thread can access the address space of the process and inherit the floating-point environment and signal mask word of the calling thread, but the thread's pending signal set is cleared.
//create.c
#include<stdio.h>
#include<pthread.h>

//int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
void *func1(void *arg)
{
        static int ret = 10;
        printf("t1:%ld thread is create\n",(unsigned long)pthread_self());
        printf("t1:param is %d\n",*((int *)arg));
        pthread_exit((void *)&ret);
}

int main()
{
        int ret;
        int param = 100;
        pthread_t t1;

        int *pret = NULL;

        ret = pthread_create(&t1,NULL,func1,(void *)&param);
        if(ret == 0)
        {
                printf("main:create t1 successful\n");

        }
        printf("main:%ld\n",(unsigned long)pthread_self());

        pthread_join(t1,(void **)&pret);
        return 0;
}

Output result:

3. Code verification of thread shared memory space

//Test1.c
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>
//int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
int g_data = 0;             //Define a global variable
void *func1(void *arg)
{
	static int ret = 10;    //Define a static variable
	
	printf("t1:%ld thread is create\n",(unsigned long)pthread_self());
	printf("t1:param is %d\n",*((int *)arg));

	while(1)
	{
		printf("t1:%d\n",g_data++);
		sleep(1);
	}
	  pthread_exit((void *)&ret);

}

void *func2(void *arg)
{
        static int ret = 10;

        printf("t2:%ld thread is create\n",(unsigned long)pthread_self());
        printf("t2:param is %d\n",*((int *)arg));

	  while(1)
        {
                printf("t2:%d\n",g_data++);
                sleep(1);
        }

	  pthread_exit((void *)&ret);
}

	
int main()
{
	int ret;
	int param = 100;
	
	pthread_t t1;
	pthread_t t2;
	int *pret = NULL;
	
	ret = pthread_create(&t1,NULL,func1,(void *)&param);

	if(ret == 0)
	{
		printf("main:create t1 successful\n");
	}	
	ret = pthread_create(&t2,NULL,func2,(void *)&param);
	 if(ret == 0)
        {
                printf("main:create t2 successful\n");
        }
	  while(1)
        {
                printf("main:%d\n",g_data++);
                sleep(1);
        }
        
	printf("main:%ld\n",(unsigned long)pthread_self());
	
	pthread_join(t1,NULL);
	pthread_join(t2,NULL);
	return 0;
}

Output result:

From the above running results, we can know that threads share memory space.

4. Lock and unlock the mutex of thread synchronization

//Test3.c
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>


//int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
int g_data = 0;
pthread_mutex_t mutex;

void *func1(void *arg)
{

	int i;;
	pthread_mutex_lock(&mutex);
	for(i=0;i<5;i++)
	{
		printf("t1:%ld thread is create\n",(unsigned long)pthread_self());
	        printf("t1:param is %d\n",*((int *)arg));
		sleep(1);
	}

	pthread_mutex_unlock(&mutex);
}
void *func2(void *arg)
{
	pthread_mutex_lock(&mutex);
	
	printf("t2:%ld thread is create\n",(unsigned long)pthread_self());
        printf("t2:param is %d\n",*((int *)arg));
	
	pthread_mutex_unlock(&mutex);

}


void *func3(void *arg)
{
        pthread_mutex_lock(&mutex);

        printf("t3:%ld thread is create\n",(unsigned long)pthread_self());
        printf("t3:param is %d\n",*((int *)arg));

        pthread_mutex_unlock(&mutex);

}


int main()
{
	int ret;
	int param = 100;
	pthread_t t1;
	pthread_t t2;
	pthread_t t3;

	pthread_mutex_init(&mutex,NULL);

	int *pret = NULL;

	ret = pthread_create(&t1,NULL,func1,(void *)&param);
	if(ret == 0)
	{
		printf("main:create t1 successful\n");

	}	
	ret = pthread_create(&t2,NULL,func2,(void *)&param);


	 if(ret == 0)
        {
                printf("main:create t2 successful\n");

        }
	ret = pthread_create(&t3,NULL,func3,(void *)&param);


	 if(ret == 0)
        {
		printf("main:create t3 successful\n");
	}


	printf("main:%ld\n",(unsigned long)pthread_self());

	pthread_join(t1,NULL);
	pthread_join(t2,NULL);
	pthread_join(t3,NULL);
	pthread_mutex_destroy(&mutex);
	return 0;
}

Output result:

5. Mutexes restrict access to shared resources

Implementation function: when the global variable G_ When data equals 3, thread t1 exits

//Test3.c
#include<stdio.h>
#include<pthread.h>
 #include <unistd.h>
#include<stdlib.h>

//int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
int g_data = 0;
pthread_mutex_t mutex;

void *func1(void *arg)
{	
	printf("t1:%ld thread is create\n",(unsigned long)pthread_self());
	printf("t1:param is %d\n",*((int *)arg));
	pthread_mutex_lock(&mutex);
	while(1)
	{
		printf("t1:%d\n",g_data++);
		sleep(1);
	
		if(g_data == 3)
		{
			pthread_mutex_unlock(&mutex);
			printf("t1 quit==============\n");
			//pthread_exit(NULL);
			exit(0);
		}
	}
}
void *func2(void *arg)
{
        printf("t2:%ld thread is create\n",(unsigned long)pthread_self());
        printf("t2:param is %d\n",*((int *)arg));

	  while(1)
        {              
			 printf("t2:%d\n",g_data);
	         pthread_mutex_lock(&mutex);
			 g_data++;
			 pthread_mutex_unlock(&mutex);
			 sleep(1);
        }
}

	
int main()
{
	int ret;
	int param = 100;
	pthread_t t1;
	pthread_t t2;
	pthread_mutex_init(&mutex,NULL);

	int *pret = NULL;

	ret = pthread_create(&t1,NULL,func1,(void *)&param);
	if(ret == 0)
	{
		printf("main:create t1 successful\n");

	}	
	ret = pthread_create(&t1,NULL,func2,(void *)&param);

	 if(ret == 0)
        {
                printf("main:create t2 successful\n");
        }
	while(1)
	{
		printf("main:%d\n",g_data);
		sleep(1);
	}
	pthread_join(t1,NULL);
	pthread_join(t2,NULL);
	
	pthread_mutex_destroy(&mutex);
	return 0;
}

Output result:

6. What causes a deadlock

If a thread tries to lock the same mutex twice, it will fall into a deadlock state, but there are other less obvious ways to deadlock when using mutex. For example, when more than one mutex is used in a program, if one thread is allowed to occupy the first mutex and is blocked when trying to lock the second mutex, but the thread with the second mutex is also trying to lock the first mutex. Because two threads are requesting each other for resources owned by another thread, neither thread can run forward, resulting in deadlock.

//Test4.c
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>
#include<stdlib.h>

//int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
int g_data = 0; //Define a global variable
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;

void *func1(void *arg)
{	
	pthread_mutex_lock(&mutex1);
	pthread_mutex_lock(&mutex2);	
}
void *func2(void *arg)
{
	pthread_mutex_lock(&mutex2);
    pthread_mutex_lock(&mutex1);
}

	
int main()
{
	int ret;
	int param = 100;
	pthread_t t1;
	pthread_t t2;

	//Create two mutexes
	pthread_mutex_init(&mutex1,NULL);
	pthread_mutex_init(&mutex2,NULL);

	int *pret = NULL;

	ret = pthread_create(&t1,NULL,func1,(void *)&param);
	if(ret == 0)
	{
		printf("main:create t1 successful\n");
	}	
	ret = pthread_create(&t2,NULL,func2,(void *)&param);

	 if(ret == 0)
        {
                printf("main:create t2 successful\n");

        }
	pthread_join(t1,NULL);
	pthread_join(t2,NULL);
	pthread_mutex_destroy(&mutex1);
	pthread_mutex_destroy(&mutex2);
	return 0;
}

Output result:

You can see that the program is stuck

7. Thread condition control to realize thread synchronization

//Test5.c
#include<pthread.h>
#include <unistd.h>
#include<stdlib.h>
#include<stdio.h>
//int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
int g_data = 0;
pthread_cond_t cond;
pthread_mutex_t mutex;

void *func1(void *arg)
{	
	while(1)
	{
		pthread_cond_wait(&cond,&mutex);	
		printf("t1 run==============\n");
		printf("t1:%d\n",g_data);
		g_data = 0;
		sleep(1);
	}
}
void *func2(void *arg)
{
	  while(1)
        {              
			printf("t2:%d\n",g_data);
            pthread_mutex_lock(&mutex);
			g_data++;
			if(g_data == 3)
			{
				pthread_cond_signal(&cond);
			}
			pthread_mutex_unlock(&mutex);
			sleep(1);
        }
}

	
int main()
{
	int ret;
	int param = 100;
	pthread_t t1;
	pthread_t t2;
	pthread_mutex_init(&mutex,NULL);
	pthread_cond_init(&cond, NULL);

	ret = pthread_create(&t1,NULL,func1,(void *)&param);
	ret = pthread_create(&t2,NULL,func2,(void *)&param);

	pthread_join(t1,NULL);
	pthread_join(t2,NULL);
	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);
	return 0;
}

Output result:

2, Previous articles

1. Getting started with Linux system programming - files
2. Getting started with Linux system programming – process
3. Getting started with Linux system programming – threads
4. Getting started with Linux system programming – interprocess communication
5. Getting started with Linux system programming – network programming

Topics: Linux Unix server