Linux操作系统篇:多线程

一. Linux中线程是怎么理解的

1.1 线程概念

在Linux中,线程是在进程"内部"执行的,线程是处于进程的进程地址空间中运行,线程用到的资源都是进程的资源,线程是执行进程的一部分代码,线程是最小的执行流,执行流要执行就得有资源,资源是在物理内存中存储着,通过进程地址空间+页表的形式找到对应的资源。

1.2 重新定义线程和进程

一个或多个执行流(线程),进程地址空间,文件描述符表,页表,物理内存保存的进程资源等加一起才是进程,线程是在进程内部的执行流资源。线程是操作系统调度的基本单位。进程是资源分配的基本实体(单位)。 操作系统以进程为基本单位分配资源,进程在创建时就会携带一个线程,这个线程是是主线程。进程包含线程,进程内有一个或多个线程,一个进程有自己的一个PCB,页表,进程地址空间,文件描述符表,这都是操作系统给其分配的。

除了Linux操作系统外,管理线程是用结构体TCB描述线程并用链表或更高级的数据结构管理起来进程的线程们,TCB是置在进程的PCB中的。 而在Linux中,管理线程也是复用了管理进程的方法,在Linux中,并没有线程的概念,线程又称轻量级进程,每个线程都有自己的PCB,在之前讲进程的时候,在进程被创建的时候会有自己的一个PCB,其实这个PCB是属于进程被创建时一并被创建的主线程的。

1.3 线程周边概念

1.3.1进程VS线程

  1. 线程的创建和释放比进程的创建和释放更轻量级:更快。因为线程的创建不用创建进程地址空间,页表什么的,只需要创建一个PCB就可以,释放也是如此,只用释放一个PCB即可。

2)线程的切换比进程的切换也更轻量化:线程的切换不需要大规模的改动,只用替换CPU寄存器的数据即可,而进程的切换涉及到页表,进程地址空间等等资源的切换。还有一点是cache缓存,在CPU拿取数据是从cache拿取数据的,不会直接存内存拿取数据,内存数据是要先加载到cache中的,线程切换时,不用切换cache缓存的数据,因为进程中的线程共用一个进程地址空间,所以用到的物理内存的数据也是同一份数据。而进程的切换需要涉及到cache缓存中缓存数据的切换,这也是一个细节。

线程的优点

创建线程的代价比进程小且线程占用的资源要比进程少很多。

线程之间的切换需要操作系统做的工作要少很多。

线程的缺点 健壮性低下如果一个线程出现问题,这个进程将会崩溃。这是因为线程出异常,就是进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

编程难度提高编写与调试一个多线程程序比单线程程序困难得多。

线程最重要的两个字段是线程自己的栈空间和硬件上下文,独立的硬件上下文能体现出线程是被调度处理的基本单元,栈能让线程的资源独立。线程除了栈空间外,其他区域都是共享的。进程的第一个线程是主线程,其他线程是工作线程,进程被创建出来就有一个线程(主线程),这个主线程负责执行main函数。

二. 线程控制

Linux并没有给用户提供操作线程的系统调用接口,因为Linux并没有线程的概念,而是轻量级进程的概念。但是Linux开发人员在应用层提供了线程库pthread(实现了对轻量级进程接口的封装)。pthread库几乎在所有的Linux版本中都自带,但毕竟是第三方库,在编译时要引入头文件<pthreaad.h>并且指定 -l pthread。

2.1. 线程常用接口

cpp 复制代码
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

作用:创建线程接口

参数说明: thread:获取创建成功的线程ID,该参数是一个输出型参数。

attr:用于设置创建线程的属性,一般是传入nullptr表示使用默认属性。

start_routine:该参数是一个函数指针,线程启动后要执行的函数。

arg:传给线程例程的参数。

返回值说明: 线程创建成功返回0,失败返回错误码。

主线程调用pthread_create函数创建一个新线程,此后新线程就会跑去执行创建时给他传的函数指针对应的函数,而主线程则继续执行后续代码。

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* Routine(void* arg)
{
	char* msg = (char*)arg;
	while (1){
		printf("I am %s\n", msg);
		sleep(1);
	}
}
int main()
{
	pthread_t tid;
	pthread_create(&tid, NULL, Routine, (void*)"thread 1");
	while (1){
		printf("I am main thread!\n");
		sleep(2);
	}
	return 0;
}

当我们用ps axj命令查看当前进程的信息时,虽然此时该进程中有两个线程,但是我们看到的进程只有一个,因为这两个线程都是属于同一个进程的。

使用ps -aL命令,可以显示当前的轻量级进程。

  • 默认情况下,不带-L,看到的就是一个个的进程。
  • -L就可以查看到每个进程内的多个轻量级进程。

其中,LWP(Light Weight Process)就是轻量级进程的ID,可以看到显示的两个轻量级进程的PID是相同的,因为它们属于同一个进程。 在Linux中,LWP确实是线程的ID,但是LWP是给操作系统内核使用的,操作系统调度线程的时候采用的是LWP,而并非PID,只不过我们之前接触到的都是单线程进程,其PID和LWP是相等的,所以对于单线程进程来说,调度时采用PID和LWP是一样的。 篇thread_create函数的第一个参数是输出函数,获取线程的ID,不过这个ID不是LWP,因为LWP是给操作系统调用使用的,这个ID是给用户使用的,和LWP不一样,其实是在进程地址空间中的共享区中pthread库分配给线程的储存空间的首地址。(下面详解)。

cpp 复制代码
pthread_t pthread_self(void);

获取线程的ID

调用pthread_self函数即可获得当前线程的ID,类似于调用getpid函数获取当前进程的ID。

例如,下面代码中在每一个新线程被创建后,主线程都将通过输出型参数获取到的线程ID进行打印,此后主线程和新线程又通过调用pthread_self函数获取到自身的线程ID进行打印。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* Routine(void* arg)
{
	char* msg = (char*)arg;
	while (1){
		printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());
		sleep(1);
	}
}
int main()
{
	pthread_t tid[5];
	for (int i = 0; i < 5; i++)
    {
		char* buffer = (char*)malloc(64);
		sprintf(buffer, "thread %d", i);
		pthread_create(&tid[i], NULL, Routine, buffer);
		printf("%s tid is %lu\n", buffer, tid[i]);
	}
	while (1){
		 printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());
		 sleep(2);
	}
	return 0;
}

注意: 用pthread_self函数获得的线程ID与内核的LWP的值是不相等的,pthread_self函数获得的是用户级原生线程库的线程ID,而LWP是内核的轻量级进程ID,它们之间是一对一的关系。

cpp 复制代码
int pthread_join(pthread_t thread, void **retval);

线程等待

首先需要明确的是,一个线程被创建出来,这个线程就如同进程一般,也是需要被等待的。如果主线程不对新线程进行等待,那么这个新线程的资源也是不会被回收的。所以线程需要被等待,如果不等待会产生类似于"僵尸进程"的问题,也就是内存泄漏。

参数说明:

  • thread:被等待线程的ID。
  • retval:线程退出时的退出码信息。不关心可设置为nullptr。

返回值说明:

  • 线程等待成功返回0,失败返回错误码。
cpp 复制代码
void pthread_exit(void *retval);

终止线程

参数说明:

  • retval:线程退出时的退出码信息。

说明一下:

  • 该函数无返回值,跟进程一样,线程结束的时候无法返回它的调用者(自身)。
  • pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。

使用pthread_exit函数终止线程,并将线程的退出码设置为6666。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* Routine(void* arg)
{
	char* msg = (char*)arg;
	int count = 0;
	while (count < 5){
		printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());
		sleep(1);
		count++;
	}
	pthread_exit((void*)6666);
}
int main()
{
	pthread_t tid[5];
	for (int i = 0; i < 5; i++){
		char* buffer = (char*)malloc(64);
		sprintf(buffer, "thread %d", i);
		pthread_create(&tid[i], NULL, Routine, buffer);
		printf("%s tid is %lu\n", buffer, tid[i]);
	}
	printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());
	for (int i = 0; i < 5; i++){
		void* ret = NULL;
		pthread_join(tid[i], &ret);
		printf("thread %d[%lu]...quit, exitcode: %d\n", i, tid[i], (int)ret);
	}
	return 0;
}
cpp 复制代码
int pthread_cancel(pthread_t thread);

使用pthread_cancel函数取消某一个线程

参数说明:

  • thread:被取消线程的ID。

返回值说明:

  • 线程取消成功返回0,失败返回错误码。

线程是可以取消自己的,取消成功的线程的退出码一般是-1。

cpp 复制代码
int pthread_detach(pthread_t thread);

分离线程

默认情况下,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏。但如果我们不关心线程的返回值,join也是一种负担,此时我们可以将该线程进行分离,后续当线程退出时就会自动释放线程资源。一个线程如果被分离了,这个线程依旧要使用该进程的资源,依旧在该进程内运行,甚至这个线程崩溃了一定会影响其他线程,只不过这个线程退出时不再需要主线程去join了,当这个线程退出时系统会自动回收该线程所对应的资源。

参数说明:

  • thread:被分离线程的ID。

返回值说明:

  • 线程分离成功返回0,失败返回错误码。

例如,下面我们创建五个新线程后让这五个新线程将自己进行分离,那么此后主线程就不需要在对这五个新线程进行join了。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* Routine(void* arg)
{
	pthread_detach(pthread_self());
	char* msg = (char*)arg;
	int count = 0;
	while (count < 5){
		printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());
		sleep(1);
		count++;
	}
	pthread_exit((void*)6666);
}
int main()
{
	pthread_t tid[5];
	for (int i = 0; i < 5; i++){
		char* buffer = (char*)malloc(64);
		sprintf(buffer, "thread %d", i);
		pthread_create(&tid[i], NULL, Routine, buffer);
		printf("%s tid is %lu\n", buffer, tid[i]);
	}
	while (1){
		printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());
		sleep(1);
	}
	return 0;
}

三. 线程在原生线程库中的ID到底是什么?

进程运行时动态库被加载到内存,然后通过页表映射到进程地址空间中的共享区,此时该进程内的所有线程都是能看到这个动态库的。

我们说每个线程都有自己私有的栈,其中主线程采用的栈是进程地址空间中原生的栈,而其余线程采用的栈空间就是在共享区中开辟的。除此之外,线程库在进程地址空间的共享区内回为每个线程创建一个TCB保存线程的信息,TCB包含struct pthread,线程局部存储,栈空间。每个线程都有自己的struct pthread,当中包含了对应线程的各种属性;每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据。

每一个新线程在共享区都有这样一块区域对其进行描述,因此我们要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息。

线程ID本质就是进程地址空间共享区上的一个虚拟地址,同一个进程中所有的虚拟地址都是不同的,因此可以用它来唯一区分每一个线程。 线程ID就是自己在进程地址空间共享区TCB的首地址。

相关推荐
✿ ༺ ོIT技术༻几秒前
Linux:认识文件系统
linux·运维·服务器
blammmp7 分钟前
Java:数据结构-枚举
java·开发语言·数据结构
暗黑起源喵26 分钟前
设计模式-工厂设计模式
java·开发语言·设计模式
会掉头发28 分钟前
Linux进程通信之共享内存
linux·运维·共享内存·进程通信
WaaTong30 分钟前
Java反射
java·开发语言·反射
我言秋日胜春朝★30 分钟前
【Linux】冯诺依曼体系、再谈操作系统
linux·运维·服务器
饮啦冰美式1 小时前
22.04Ubuntu---ROS2使用rclcpp编写节点
linux·运维·ubuntu
wowocpp1 小时前
ubuntu 22.04 server 安装 和 初始化 LTS
linux·运维·ubuntu
Huaqiwill1 小时前
Ubuntun搭建并行计算环境
linux·云计算