马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
线程的简单相识
之前我们相识过 task_struct 是用于描述进程的焦点数据结构。它包含了一个进程的所有紧张信息,而且在进程的生命周期内保持更新。我们想要获取进程相关信息往往从这里得到。
- 在Linux中,线程的实现方式与进程类似,每个线程都有一个task_struct结构体,用于存储线程的信息。线程的task_struct结构体比进程的task_struct结构体要小,包含的信息更少。
- 进程是操作体系资源分配的最小单位,而线程是操作体系调理的最小单位。
- 线程之间的切换通常比进程切换更高效,因为线程共享进程的资源,不需要像进程切换那样生存和恢复大量的资源信息。
windows中的线程和Linux中线程区别
在Windows操作体系,内核中有真线程,名为TCB :线程控制块。需要维护进程与线程之间的调理关系算法,这过于复杂。
在Linux中,由于线程的控制块与进程控制块相似性非常高,以是直接复用了PCB的结构体——task_struct ,用PCB模拟线程的TCB。以是Linux没有真正意义上的线程,而是用进程方案模拟的线程。这样做的利益是复用代码和结构更简单,好维护,效率更高,也更安全。
线程的特性
多线程的优点
- 同时执行多个任务: 多线程允许步调同时执行多个任务,而不是按顺序一个接一个地执行。
- 进步响应速度: 对于需要处置惩罚大量并发请求的步调(如Web服务器),多线程可以显著进步步调的响应速度。
- 更小开销,更快的切换: 线程切换的开销也比进程切换要小,这使得多线程步调可以更高效地举行任务切换。
- 在等待慢速I/O操作结束的同时,步调可执行其他的盘算任务
- 盘算密集型应用,为了能在多处置惩罚器体系上运行,将盘算分解到多个线程中实现,可以有效进步盘算效率,但是注意:线程不是越多越好,正常情况下最合适的原则是:进程/线程与cpu个数/核数保持一致
多线程的缺点
- 共享资源竞争: 多个线程共享进程的地点空间,当它们同时访问和修改共享资源时,大概会出现竞争条件,导致数据不一致或步调错误。
- 同步机制复杂: 为相识决线程安全问题,需要利用线程同步机制(如互斥锁、条件变量等),这些机制会增加编程的复杂性,容易出错。
- 上下文切换开销: 线程切换需要生存和恢复线程的上下文,这会消耗肯定的CPU时间。过多的线程切换大概会低落步调的效率。
- 线程间依赖: 线程之间大概存在依赖关系,一个线程的执行大概会影响到其他线程的执行。如果处置惩罚不妥,大概会导致步调出现意外错误。
- 调试困难:多线程步调的执行顺序是不确定的,这使得步调的调试变得更加困难。由于线程的执行受到多种因素的影响,一些错误大概很难复现,增加了调试的难度。
PROSIX线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要利用这些函数库,要通过引入头文件 <pthread.h>
而且链接这些线程函数库时要利用编译器命令的“-lpthread”选项
之前我们利用的都是linux中的基础标准库,这些标准库在编译的时间会自动帮我们举行链接,我们只需要包含一个头文件,不需要手动链接。但是对于线程库的话默认不会帮我们链接,除了需要我们步调中包含对应头文件,还需要编译的时间手动链接。
包含头文件和编译时链接区分
- 包含头文件(#include): 这是在源代码文件中做的,用于告诉编译器步调中利用了哪些函数、变量、类型等。头文件通常包含函数声明、宏定义、结构体定义等。
- 编译时链接: 这是在编译命令中做的,用于告诉链接器将步调中利用的函数和变量与它们在库文件中的具体实现链接起来。库文件通常包含编译好的函数和变量的二进制代码。
包含头文件的作用:
- 让编译器理解代码: 头文件相当于一个“接口阐明书”,告诉编译器步调中利用了哪些“零件”(函数、变量等),以及这些“零件”的规格(参数类型、返回值类型等)。
- 提供类型检查: 编译器可以根据头文件中的声明来检查步调中函数和变量的利用是否精确,避免类型错误。
编译时链接的作用:
- 天生可执行文件: 链接器将步调中利用的函数和变量与它们在库文件中的实现“组装”起来,天生最终的可执行文件。
- 链接外部代码: 步调中利用的某些函数和变量大概不是由本身编写的,而是由其他人或构造提供的,这些代码通常放在库文件中。链接器将这些外部代码链接到步调中,使得步调可以利用这些外部功能。
线程创建
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 类型变量的指针,用于存储新创建线程的 ID。
- attr: 一个指向 pthread_attr_t 类型变量的指针,用于设置新线程的属性。如果设置为 NULL,则利用默认属性。通常我们设置成NULL就行
- start_routine: 一个函数指针,表现新线程要执行的函数。该函数必须接受一个 void * 类型的参数,并返回一个 void * 类型的值。
- arg: 一个指向 void * 类型变量的指针,表现转达给 start_routine 线程函数的参数。如果我们不需要转达任何数据给线程函数,完全可以将它设置为 NULL
返回值:
- 成功: 返回 0。
- 失败: 返回一个非零的错误码,表现创建线程失败的缘故原由。
线程函数的定义:
线程函数必须符合 void *(*start_routine)(void *) 的函数署名,即吸收一个 void * 类型的参数并返回一个 void * 类型的值。
简单的线程创建例子
例子比较简单,主要就是创建了一个新的线程,然后主线程和新线程同时执行,主线程输出26个英文字母,新线程输出数字0-9。
编译代码的时间记得加上编译链接选项:-lpthread
- #include <stdio.h>
- #include <pthread.h>
- void *thread_function(void *arg) {
- int i=0;
- while(1){
- fprintf(stderr,"%d",i);
- i++;
- if(i==10){
- i=0;
- }
- }
- }
- int main() {
- pthread_t thread_id;
- int ret = pthread_create(&thread_id, NULL, thread_function, NULL);
- if (ret != 0) {
- perror("pthread_create failed");
- return 1;
- }
- printf("Thread created successfully\n");
- int i=0;
- while(1){
- fprintf(stderr,"%c",'a'+i);
- i++;
- if(i==26){
- i=0;
- }
- }
- return 0;
- }
复制代码 最终看到的效果就是主线程和新线程双线执行输出。
线程间共享资源与不共享资源
共享资源
- 堆(Heap): 存储进程中动态分配的对象。
- 代码段(Code Segment): 存储步调的指令。
- 数据段(Data Segment): 存储进程的全局变量和静态变量。
- 文件描述符表: 存储进程打开文件的信息。
- 信号处置惩罚函数: 用于处置惩罚进程吸收到的信号。
不共享资源
- 栈(Stack): 每个线程都有本身的栈,用于存储局部变量、函数调用信息等。
- 寄存器(Registers): 每个线程都有一组寄存器,用于存储线程执行过程中的临时数据。
- 线程 ID: 每个线程都有一个唯一的线程 ID,用于标识线程。
利用共享资源时需要注意的问题:
- 竞态条件(Race Condition): 多个线程同时访问和修改共享资源时,大概会导致数据不一致的问题。
- 死锁(Deadlock): 多个线程互相称待对方释放资源,导致步调无法继续执行的问题。
解决竞态条件和死锁问题的方法:
- 互斥锁(Mutex): 用于保护共享资源,同一时候只允许一个线程访问。
- 条件变量(Condition Variable): 用于线程之间的同步,当一个线程等待某个条件满足时,可以利用条件变量举行阻塞。
- 信号量(Semaphore): 用于控制同时访问共享资源的线程数目。
多线程共享资源同时访问出错例子
场景:假设有一个共享的计数器变量 counter,初始值为 0。现在有多个线程同时对 counter 举行加 1 操作。
预期效果:由于有 10 个线程,每个线程执行 100000 次加 1 操作,因此最终的 counter 值应该为 10 * 100000 = 1000000。
实际效果:实际运行效果通常会小于 10000。
缘故原由分析:
当多个线程同时访问 counter 变量时,由于线程切换的存在,大概会导致以下情况:
- 线程 A 读取 counter 的值。
- 线程 A 被切换出去,线程 B 开始执行。
- 线程 B 读取 counter 的值。
- 线程 B 将 counter 的值加 1。
- 线程 B 被切换出去,线程 A 继续执行。
- 线程 A 将之前读取的 counter 值加 1,并写回。
这样,线程 A 和线程 B 都只举行了一次加 1 操作,但 counter 的值只增加了 1,而不是 2。这种情况称为竞态条件。
解决方法:可以利用互斥锁(Mutex)来保护共享资源 counter,确保同一时候只有一个线程可以访问它。
通过利用互斥锁,可以保证每个线程对 counter 的加 1 操作都是原子性的,从而避免竞态条件,得到精确的效果。
- #include <stdio.h>
- #include <pthread.h>
- #define NUM_THREADS 10
- #define INCREMENTS 100000
- int counter = 0;
- void *increment_counter(void *arg) {
- for (int i = 0; i < INCREMENTS; i++) {
- counter++;
- }
- return NULL;
- }
- int main() {
- pthread_t threads[NUM_THREADS];
- for (int i = 0; i < NUM_THREADS; i++) {
- pthread_create(&threads[i], NULL, increment_counter, NULL);
- }
- //作用是等待多个线程执行结束。它通常出现在多线程程序中,
- //用于确保主线程在所有子线程完成任务后才退出。
- for (int i = 0; i < NUM_THREADS; i++) {
- pthread_join(threads[i], NULL);
- }
- printf("Expected counter value: %d\n", NUM_THREADS * INCREMENTS);
- printf("Actual counter value: %d\n", counter);
- return 0;
- }
复制代码
线程退出
pthread_exit 函数先容
函数作用:结束调用该函数的线程,同时还可以转达一个退出状态值给其他线程
头文件:#include <pthread.h>
函数原型:
- void pthread_exit(void *retval);
复制代码 参数:
retval 参数可以用于转达一个退出状态值给其他线程,通常通过 pthread_join() 函数来吸收这个值。如果不需要转达退出状态,可以将 retval 设置为 NULL。
僵尸线程
首先我们往返顾一下僵尸进程:
僵尸进程
- 当一个进程结束运行时,内核不会立即释放它占用的所有资源,而是将其状态设置为僵尸态(Zombie)。
- 僵尸进程会生存一些基本信息(如进程ID、退出状态等),以便父进程可以获取到子进程的退出信息。
- 父进程需要调用 wait() 或 waitpid() 等函数来接纳僵尸进程的资源,否则僵尸进程会一直存在,占用体系资源。
然后我们来看一下线程的僵尸态
与进程类似,当一个线程结束运行时,它也会进入一个类似于僵尸态的状态。
- 状态生存: 线程退出后,其占用的大部分资源(如栈空间)会被自动接纳,但是线程也会生存一些状态信息,比方退出状态,以便其他线程(通常是主线程)可以通过 pthread_join() 函数来获取。
- 接纳方式: 线程的“接纳”主要通过 pthread_join() 函数来实现。当主线程调用 pthread_join() 函数等待某个线程结束时,实际上就是在“接纳”该线程的状态信息。
僵尸态的紧张性
- 无论是进程照旧线程,僵尸态的存在都是为了让父进程或主线程能够获取到子进程或子线程的退出信息。
- 这些退出信息大概包含执行效果、错误码等,对于步调的调试和错误处置惩罚非常有资助。
线程接合
pthread_join函数先容
函数作用:阻塞当前线程,直到指定的线程执行完毕,实用于线程间的同步
头文件:#include <pthread.h>
函数原型:
- int pthread_join(pthread_t thread, void **retval);
复制代码 参数:
thread:要等待的线程的线程 ID。这是一个由 pthread_create 创建的线程 ID。
retval:这是一个指向指针的指针,函数会把目的线程的退出状态通过该指针返回。如果目的线程没有返回任何值,可以转达 NULL。
实在我蛮不理解这里为什么利用二级指针的,在我看来pthread_exit 转达的参数是一个一级指针,但是这里pthread_join选择一个一级指针来对应赋值就可以了,不太理解为什么要利用二级指针
返回值:
- 成功: 返回 0。
- 失败: 返回一个非零的错误码。
实用场景
- 同步线程: 当一个线程需要等待另一个线程完成后才气继续执行时,可以利用 pthread_join() 函数举行同步。
- 获取线程返回值: 有些线程会返回一个值,表现它们的执行效果。可以利用 pthread_join() 函数获取这个返回值。
- 资源接纳: 当一个线程结束后,它的资源不会立即被释放。需要调用 pthread_join() 函数才气接纳这些资源。
线程分离态(相识)
线程的默认状态
默认情况下,新创建的线程都处于非分离态 (Joinable State)。这意味着:
- 资源接纳: 当一个线程结束运行时,它所占用的资源(如栈空间)不会立即被释放,而是会生存一段时间,直到有其他线程调用 pthread_join() 函数来“接纳”该线程。
- 获取退出状态: 其他线程可以通过调用 pthread_join() 函数来等待该线程结束,并获取它的退出状态。
什么是分离态?
分离态 (Detached State) 是一种特殊的线程状态。当一个线程被设置为分离态时,它与创建它的线程(通常是主线程)之间的关系就会被“分离”。这意味着:
- 自动资源接纳: 当一个分离态线程结束运行时,它所占用的资源会被自动接纳,无需其他线程调用 pthread_join() 函数。
- 无法获取退出状态: 其他线程无法通过 pthread_join() 函数来等待分离态线程的结束,也无法获取它的退出状态。
如何设置线程为分离态?
方法一
- pthread_detach(thread_id);
复制代码 这个函数可以直接将指定线程设置为分离状态。
但是你大概会想如何得到一个线程自身的线程tid呢?实在很简单,有一个函数pthread_self可以很容易帮我们获取到当前线程的tid。
这个函数在后面一篇文章中也会详细讲到,这里只是简单提一下。
函数原型:pthread_t pthread_self(void);
以是我们常常将 pthread_self 配合 pthread_detach 一起利用,像下面这样:
- pthread_detach(pthread_self());
复制代码
方法二
利用pthread_create函数创建线程的时间,有一个参数可以设置新创建的线程的属性。我们可以凭借这个参数来设置线程为分离态,这种方式相比于方法一,更加麻烦,但是有着本身的优点,之后我们会详细讲到,这里不详细叙述。
实用场景
有些情况下,我们对于某些线程来说不关心它的返回状态,而且也不想要利用pthread_join来阻塞等待接纳这个死后的僵尸线程。那么此时我们就可以把这个线程设置因素离态度,当线程殒命自动释放,不需要其他线程调用pthread_join往返收这个僵尸进程的资源。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |