2024年Linux最全Linux 多线程原理深剖_线程的布局,2024年最新成功跳槽百度工资从15K涨到28K

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前在阿里

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以点击这里获取!

  1. 性能损失,如果计算密集型线程的任务比可用的处理器多,会产生较大的性能损失,性能包括额外的同步和调度开销,但是可用资源是不变的
  2. 健壮性降低,多线程需要全面的思考,因时间分配上的细微偏差或者因共享了不该共享的变量会造成不良影响,也就是说线程之间是缺乏保护的
  3. 缺乏访问控制,进程是访问控制的基本粒度,线程中调用 os 函数会对整个进程产生影响
  4. 难度提高,调试和编写都涉及更复杂

线程异常🤔

比如一个线程出现除0,野指针问题,会直接全部垮掉;且线程是进程的执行分支名,线程出现异常,就像进程出现异常一样,会触发信号机制,此时所有的线程会全部退出。因此合理使用多线程,能提高 CPU 密集型程序的执行效率与用户体验

进程与线程🤔

进程是分配系统资源的基本单位,线程是系统调度的基本单位
在这里插入图片描述

线程共享进程数据,但也拥有自己的一部分数据,比如线程ID,线程栈(每个线程都有临时的数据,需要压栈出栈),全局变量 errno,信号屏蔽字与调度优先级

多线程共享🤔

同一个地址空间里面,代码段和数据段和数据段都是共享的,如果定义一个函数在各线程中都可以调用。如果定义一个全局变量,在各线程中都可以访问到

各线程还共享一些进程资源和环境,文件描述符表(进程打开一个文件后,其他线程也能够看到)每种信号的处理方式,用户ID和组ID

Linux线程控制🤔

POSIX线程库😋

pthread线程库是应用层的原生线程库:

应用层指的是这个线程库并不是系统接口直接提供的,而是由第三方帮我们提供的。原生指的是大部分Linux系统都会默认带上该线程库。
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的,要使用这些函数库,就要引入头文件<pthreaad.h>,链接这些线程函数库时,要使用编译器命令的“-lpthread”选项

传统的一些函数是,成功返回0,失败返回-1,并且对全局变量 errno 赋值以指示错误。pthreads 出错时不会设置全局变量 errno,因为大部分POSIX函数会这样做,而是将错误代码通过返回值返回。
pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于 pthreads 的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的 errno 变量的开销更小

线程的创建😋

创建线程用到 pthread_create 函数:

int pthread\_create(pthread_t \*thread, const pthread_attr_t \*attr, void \*(\*start_routine) (void \*), void \*arg);

thread:获取创建成功的线程ID,该参数是一个输出型参数
attr:用于设置创建线程的属性,传入NULL表示使用默认属性
start_routine:该参数是一个函数地址,表示线程例程,即线程启动后要执行的函数。
arg:传给线程例程的参数

当一个程序启动时,就有一个进程被操作系统创建,与此同时一个线程也立刻运行,这个线程就叫做主线程,主线程产生其他子线程,主线程一般最后要完成某些操作,比如各种关闭动作

主线程调用 pthread_create 创建一个新线程,此后新线程会跑去执行自己的代码,而主线程则继续执行后续代码:

#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 -aL命令可查看当前的轻量级进程(不带 -L 是一个个的进程;带 -L 是每个进程内的多个轻量级进程)

在这里插入图片描述
这里的 LWP 就是轻量级进程 ID,可以看到显示的两个轻量级进程的 PID 相同,因为它们属于同一个进程。

注意

\color{red} {注意}

注意在应用层现场其实就是 LWP,操作系统实际调度的是 LWP 而并非 PID,之前因为我一直是作为单线程进程在写,所以 PID 和 LWP 是相等的,所以单线程下调度谁都可以

上面是主线程创建一个新线程,我们让主线程一次创建多个新线程,并让每一个新线程都去执行同一个函数,也就是说该函数是会被重入的:

#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\n", msg, getpid(), getppid());
		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);
	}
	while (1){
		printf("I am main thread...pid: %d, ppid: %d\n", getpid(), getppid());
		sleep(2);
	}
	return 0;
}

这里就可以看到 5 个创建成功的线程:

在这里插入图片描述
因为都属于同一个进程,所以他们的 PID 和 PPID 也都是一样的,此时使用 ps -aL 就能看到 6 个轻量级进程:

在这里插入图片描述

获取线程id😎

我们可以通过 pthread_self 函数获取线程 id,类似于 getpid 获取当前进程ID:

pthread_t pthread\_self(void);

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

#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

线程等待😋

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

等待线程函数 pthread_join:

int pthread\_join(pthread_t thread, void \*\*retval);

thread:被等待线程的ID,retval:线程退出时的退出码信息,线程等待成功返回0,失败返回错误码

retval

  1. 如果thread线程通过return返回,retval 所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用 pthread_cancel 异常终止掉,retval 所指向的单元里存放的是常数PTHREAD_CANCELED
  3. 如果thread线程是自己调用 pthread_exit 终止的,retval 所指向的单元存放的是传给 pthread_exit 的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传 NULL 给 retval 参数

grep命令进行查找,可以发现PTHREAD_CANCELED实际上就是头文件<pthread.h>里面的一个宏定义,它本质就是 -1:

grep -ER "PTHREAD\_CANCELED" /usr/include/

在这里插入图片描述
下面代码中我们先不关心线程退出信息,直接将 pthread_join 的第二次参数设置为NULL,等待线程后打印该线程的编号以及线程ID:

#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++;
	}
	return NULL;
}
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++){
		pthread\_join(tid[i], NULL);
		printf("thread %d[%lu]...quit\n", i, tid[i]);
	}
	return 0;
}

在这里插入图片描述
线程退出时如何获取退出码呢?我们将线程退出时的退出码设置为某个特殊的值,并在成功等待线程后将该线程的退出码进行输出

#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++;
	}
	return (void\*)2022;
}
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;
}

在这里插入图片描述
pthread_join 默认是以阻塞的方式进行线程等待的,那么问题来了:为什么线程退出时只能拿到线程的退出码?这是什么问题呢?比如我们可以通过 wait 或是waitpid 的输出型参数 status,获取到退出进程的退出码、退出信号以及core dump标志。就是说只能拿到退出码难道线程就不会出现异常吗?

线程当然也会出现异常,线程退出的情况有三种:

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

因此我们也需要考虑线程异常终止的情况,但是 pthread_join 无法获取线程异常退出的信息。因为线程是进程内的一个执行分支,如果进程中的某个线程崩溃了,那么整个进程也会因此而崩溃,此时我们根本没办法执行 pthread_join,因为整个进程已经退出了

我们在线程的执行例程当中制造一个除零错误,当某一个线程执行到此处时就会崩溃,进而导致整个进程崩溃:

#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++;
		int a = 1 / 0; //error
	}
	return (void\*)2022;
}
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;
}

结果如下,程序直接暴毙了:

在这里插入图片描述
所以 pthread_join 只能获取到线程正常退出时的退出码,用于判断线程的运行结果是否正确。

线程终止😋

只终止某个线程而不是终止整个进程,可以有三种方法:

  1. 线程函数处进行return。
  2. 线程可以自己调用pthread_exit函数终止自己。
  3. 一个线程可以调用pthread_cancel函数终止同一进程中的另一个线程。

线程函数处进行

r

e

t

u

r

n

\color{red} {线程函数处进行return}

线程函数处进行return

注意在线程中使用return代表当前线程退出,但是在main函数中使用return代表整个进程退出

#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);
	}
	return (void\*)0;
}
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());

	return 0;
}

运行代码,并不能看到新线程执行的打印操作,因为主线程的退出导致整个进程退出了

在这里插入图片描述

p

t

h

r

e

a

d

e

x

i

t

函数

\color{red} {pthread_exit函数}

pthreade​xit函数

void pthread\_exit(void \*retval);

retval:线程退出时的退出码信息,该函数无返回值,跟进程一样结束时无法返回自身。

pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是 malloc 分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了

这里我们使用 pthread_exit 终止线程,并将退出码设置为6666:

#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;
}

在这里插入图片描述

p

t

h

r

e

a

d

c

a

n

c

e

l

函数

\color{red} {pthread_cancel函数}

pthreadc​ancel函数

int pthread\_cancel(pthread_t thread);

这个函数代表着即线程是可以被取消的,thread 就是被取消线程的ID,线程取消成功返回0,失败返回错误码。线程是可以取消自己的,甚至新线程也可以取消主线程,取消成功的线程的退出码一般是 -1:

#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\_cancel(pthread\_self());


**先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前在阿里**

**深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

**因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。**
![img](https://img-blog.csdnimg.cn/img_convert/3b624040bab52ae10c4c0f4c358c5a0d.png)
![img](https://img-blog.csdnimg.cn/img_convert/f966c3e7da96436a1d6d7f7a85e9db81.png)
![img](https://img-blog.csdnimg.cn/img_convert/25de80297716baef6195544068278197.png)
![img](https://img-blog.csdnimg.cn/img_convert/f7bdb13e8d3eef6f43bade49ad63d181.png)
![img](https://img-blog.csdnimg.cn/img_convert/8dbe43ec64bb63070d27b7ed36db1337.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

%lu\n", msg, getpid(), getppid(), pthread\_self());
		sleep(1);
		count++;
		pthread\_cancel(pthread\_self());


**先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前在阿里**

**深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

**因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。**
[外链图片转存中...(img-pyDM0jTv-1715160765291)]
[外链图片转存中...(img-MeYju2N9-1715160765292)]
[外链图片转存中...(img-O5gUuEgL-1715160765292)]
[外链图片转存中...(img-6IO4GdE7-1715160765292)]
[外链图片转存中...(img-dcNR9lBm-1715160765293)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

  • 13
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值