线程:基础

线程是操作系统调度的基本单位,是进程中的实际运行单元,可以并发执行实现多任务。线程ID用于区分同一进程中的不同线程,线程创建、终止和线程安全是多线程编程中的关键概念。线程安全包括线程栈、可重入函数、一次性初始化和线程局部存储,确保在多线程环境下正确运行。线程与信号的交互需要注意信号掩码和异步信号安全函数的使用,以防止并发问题和死锁。
摘要由CSDN通过智能技术生成

线程

6.1 线程概述

6.1.1 线程概念

什么是线程?
线程是参与系统调度的最小单位。它被包含在进程之中,是进程中的实际运行单位。一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务。譬如某应用程序设计了两个需要并发运行的任务 task1 和 task2,可将两个不同的任务分别放置在两个线程中。
线程是如何创建起来的?
当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread),因为它是程序一开始时就运行的线程。应用程序都是以 main()做为入口开始运行的,所以 main()函数就是主线程的入口函数,main()函数所执行的任务就是主线程需要执行的任务。
所以由此可知,任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程,譬如前面章节内容中所编写的所有应用程序都是单线程程序,它们只有主线程;既然有单线程进程,那自然就存在多线程进程,所谓多线程指的是除了主线程以外,还包含其它的线程,其它线程通常由主线程来创建(调用pthread_create 创建一个新的线程),那么创建的新线程就是主线程的子线程。
主线程的重要性体现在两方面:
⚫ 其它新的线程(也就是子线程)是由主线程创建的;
⚫ 主线程通常会在最后结束运行,执行各种清理工作,如回收各个子线程。
线程的特点?
线程是程序最基本的运行单位,而进程不能运行,真正运行的是进程中的线程。当启动应用程序后,系统就创建了一个进程,可以认为进程仅仅是一个容器,它包含了线程运行所需的数据结构、环境变量等信息。
同一进程中的多个线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack,我们称为线程栈),自己的寄存器环境(register context)、自己的线程本地存储(thread-local storage)。
在多线程应用程序中,通常一个进程中包括了多个线程,每个线程都可以参与系统调度、被 CPU 执行,
线程具有以下一些特点:
⚫ 线程不单独存在、而是包含在进程中;
⚫ 线程是参与系统调度的基本单位;
⚫ 可并发执行。同一进程的多个线程之间可并发执行,在宏观上实现同时运行的效果;
⚫ 共享进程资源。同一进程中的各个线程,可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等。
线程与进程?
进程创建多个子进程可以实现并发处理多任务(本质上便是多个单线程进程),多线程同样也可以实现(一个多线程进程)并发处理多任务的需求,那我们究竟选择哪种处理方式呢?首先我们就需要来分析下多进程和多线程两种编程模型的优势和劣势。
多进程编程的劣势:
⚫ 进程间切换开销大。多个进程同时运行(指宏观同时运行,无特别说明,均指宏观上),微观上依然是轮流切换运行,进程间切换开销远大于同一进程的多个线程间切换的开销,通常对于一些中小型应用程序来说不划算。
⚫ 进程间通信较为麻烦。每个进程都在各自的地址空间中、相互独立、隔离,处在于不同的地址空间中,因此相互通信较为麻烦。
解决方案便是使用多线程编程,多线程能够弥补上面的问题:
⚫ 同一进程的多个线程间切换开销比较小。
⚫ 同一进程的多个线程间通信容易。它们共享了进程的地址空间,所以它们都是在同一个地址空间中,通信容易。
⚫ 线程创建的速度远大于进程创建的速度。
⚫ 多线程在多核处理器上更有优势!
多线程也有它的缺点、劣势,譬如多线程编程难度高,对程序员的编程功底要求比较高,因为在多线程环境下需要考虑很多的问题,例如线程安全问题、信号处理的问题等,编写与调试多线程程序比单线程程序困难得多。

6.1.2 并发和并行

对于串行比较容易理解,它指的是一种顺序执行,譬如先完成 task1,接着做 task2、直到完成 task2,然后做 task3、直到完成 task3……依次按照顺序完成每一件事情,必须要完成上一件事才能去做下一件事,只有一个执行单元,这就是串行运行。
在这里插入图片描述

并行与串行则截然不同,并行指的是可以并排/并列执行多个任务,这样的系统,它通常有多个执行单元,所以可以实现并行运行,譬如并行运行 task1、task2、task3。
在这里插入图片描述
并行运行并不一定要同时开始运行、同时结束运行,只需满足在某一个时间段上存在多个任务被多个执行单元同时在运行着。
相比于串行和并行,并发强调的是一种时分复用,与串行的区别在于,它不必等待上一个任务完成之后在做下一个任务,可以打断当前执行的任务切换执行下一个任何,这就是时分复用。在同一个执行单元上,将时间分解成不同的片段(时间片),每个任务执行一段时间,时间一到则切换执行下一个任务,依次这样轮训(交叉/交替执行),这就是并发运行。如下图所示:
在这里插入图片描述
在网络上看到了很多比较有意思、形象生动的比喻,用来说明串行、并行以及并发这三个概念的区别,这里截取其中的一个:
⚫ 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接电话,这就说明你不支持并发也不支持并行,仅仅只是串行。
⚫ 你吃饭吃到一半,电话来了,你停下吃饭去接了电话,电话接完后继续吃饭,这说明你支持并发。
⚫ 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
这里再次进行总结:
⚫ 串行:一件事、一件事接着做
⚫ 并发:交替做不同的事;
⚫ 并行:同时做不同的事
对于单核处理器系统来说,它只有一个执行单元(譬如 I.MX6U 硬件平台,单核 Cortex-A7 SoC),只能采用并发运行系统中的线程,而肯定不可能是串行。
同时运行
计算机处理器运行速度是非常快的,在单个处理核心虽然以并发方式运行着系统中的线程(微观上交替/交叉方式运行不同的线程),但在宏观上所表现出来的效果是同时运行着系统中的所有线程,因为处理器的运算速度太快了,交替轮训一次所花费的时间在宏观上几乎是可以忽略不计的,所以表示出来的效果就是同时运行着所有线程。
这就好比现实生活中所看到的一些事情,它所给带来的视角效果,譬如一辆车在高速上行驶,有时你会感觉到车的轮毂没有转动,一种视角暂留现象,因为车轮转动速度太快了,人眼是看不清的,会感觉车轮好像是静止的,事实上,车轮肯定是在转动着。

6.2 线程 ID

就像每个进程都有一个进程 ID 一样,每个线程也有其对应的标识,称为线程 ID。进程 ID 在整个系统中是唯一的,但线程 ID 不同,线程 ID 只有在它所属的进程上下文中才有意义。
进程 ID 使用 pid_t 数据类型来表示,它是一个非负整数。而线程 ID 使用 pthread_t 数据类型来表示,一个线程可通过库函数 pthread_self()来获取自己的线程 ID,其函数原型如下所示:

#include <pthread.h>
pthread_t pthread_self(void);

6.3 创建线程

主线程可以使用库函数 pthread_create()负责创建一个新的线程,创建出来的新线程被称为主线程的子线程,其函数原型如下所示:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

thread:pthread_t 类型指针,当 pthread_create()成功返回时,新创建的线程的线程 ID 会保存在参数 thread所指向的内存中。
attr:pthread_attr_t 类型指针,通常参数 attr 设置为 NULL。
start_routine:参数 start_routine 是一个函数指针,指向一个函数,新创建的线程从 start_routine()函数开始运行,该函数返回值类型为void *,并且该函数的参数只有一个void *,其实这个参数就是pthread_create()函数的第四个参数 arg。如果需要向 start_routine()传递的参数有一个以上,那么需要把这些参数放到一个结构体中,然后把这个结构体对象的地址作为 arg 参数传入。
arg:传递给 start_routine()函数的参数。一般情况下,需要将 arg 指向一个全局或堆变量,意思就是说在线程的生命周期中,该 arg 指向的对象必须存在,否则如果线程中访问了该对象将会出现错误。当然也可将参数 arg 设置为 NULL,表示不需要传入参数给 start_routine()函数。
线程创建成功,新线程就会加入到系统调度队列中,获取到 CPU 之后就会立马从 start_routine()函数开始运行该线程的任务;调用 pthread_create()函数后,通常我们无法确定系统接着会调度哪一个线程来使用CPU 资源,先调度主线程还是新创建的线程呢(而在多核 CPU 或多 CPU 系统中,多核线程可能会在不同的核心上同时执行)?如果程序对执行顺序有强制要求,那么就必须采用一些同步技术来实现。这与前面学习父、子进程时也出现了这个问题,无法确定父进程、子进程谁先被系统调度。
使用示例
使用 pthread_create()函数创建一个除主线程之外的新线程,示例代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg) {
   
 printf("新线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
 return (void *)0; }
int main(void) {
   
 pthread_t tid;
 int ret;
 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
 if (ret) {
   
 fprintf(stderr, "Error: %s\n", strerror(ret));
 exit(-1);
 }
 printf("主线程: 进程 ID<%d> 线程 ID<%lu>\n", getpid(), pthread_self());
 sleep(1);
 exit(0);
}

主线程休眠了 1 秒钟,原因在于,如果主线程不进行休眠,它就可能会立马退出,这样可能会导致新创建的线程还没有机会运行,整个进程就结束了。
在主线程和新线程中,分别通过 getpid()和 pthread_self()来获取进程 ID 和线程 ID,将结果打印出来,运行结果如下所示:
在这里插入图片描述
编译时出现了错误,提示“对‘pthread_create’未定义的引用”,示例代码确实已经包含了<pthread.h>头文件,但为什么会出现这样的报错,仔细看,这个报错是出现在程序代码链接时、而并非是编译过程,所以可知这是链接库的文件,如何解决呢?

gcc -o testApp testApp.c -lpthread

使用-l 选项指定链接库 pthread,原因在于 pthread 不在 gcc 的默认链接库中,所以需要手动指定。再次编译便不会有问题了,如下:
在这里插入图片描述
从打印信息可知,正如前面所介绍那样,两个线程的进程 ID 相同,说明新创建的线程与主线程本来就属于同一个进程,但是它们的线程 ID 不同。从打印结果可知,Linux 系统下线程 ID 数值非常大,看起来像是一个指针。

6.4 终止线程

在示例代码 11.3.1 中,我们在新线程的启动函数(线程 start 函数)new_thread_start()通过 return 返回之后,意味着该线程已经终止了,除了在线程 start 函数中执行 return 语句终止线程外,终止线程的方式还有 pthread_exit()函数;其函数原型如下所示:

#include <pthread.h>
void pthread_exit(void *retval);

调用 pthread_exit()相当于在线程的 start 函数中执行 return 语句,不同之处在于,可在线程 start 函数所调用的任意函数中调用 pthread_exit()来终止线程。如果主线程调用了 pthread_exit(),那么主线程也会终止,但其它线程依然正常运行,直到进程中的所有线程终止才会使得进程终止。
使用示例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg) {
   
 printf("新线程 start\n");
 sleep(1);
 printf("新线程 end\n");
 pthread_exit(NULL);
}
int main(void) {
   
 pthread_t tid;
 int ret;
 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
 if (ret) {
   
 fprintf(stderr, "Error: %s\n", strerror(ret));
 exit(-1);
 }
 printf("主线程 end\n");
 pthread_exit(NULL);
 exit(0);
}

新线程中调用 sleep()休眠,保证主线程先调用 pthread_exit()终止,休眠结束之后新线程也调用pthread_exit()终止,编译测试看看打印结果:
在这里插入图片描述
正如上面介绍到,主线程调用 pthread_exit()终止之后,整个进程并没有结束,而新线程还在继续运行。

6.5 回收线程

在父、子进程当中,父进程可通过 wait()函数(或其变体 waitpid())阻塞等待子进程退出并获取其终止状态,回收子进程资源;而在线程当中,也需要如此,通过调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源;pthread_join()函数原型如下所示:

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

thread:pthread_join()等待指定线程的终止,通过参数 thread(线程 ID)指定需要等待的线程;
retval:如果参数 retval 不为 NULL,则 pthread_join()将目标线程的退出状态(即目标线程通过pthread_exit()退出时指定的返回值或者在线程 start 函数中执行 return 语句对应的返回值)复制到retval 所指向的内存区域;如果目标线程被 pthread_cancel()取消,则将 PTHREAD_CANCELED 放在retval 中。如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL。
使用示例

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
static void *new_thread_start(void *arg) {
   
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值