目录
一丶线程的概念
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” [1] 。
1丶多线程的用途
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
使用我们自己术语是怎么理解多线程~比如我们在再APP商店下载一些软件,这些软件可以同时被下载,而不是一一等待着这个软件完成后再执行另一个下载。我们在打游戏的时候,你在刷野怪,你在打野怪的同时你自身在掉血,野怪也在掉血你同时又用了一个药水,你的血量又加上来了。想象一下这些场景是如何在一个时间线内完成的。这里就多线程并发完成的~
![]()
这是我们之前学习进程总会用到的一张图。多线程的生成就是基于在进程之上。假如我们又创建一些进程, 但是并不创建地址空间只生成task_struct 然后共用一个地址空间。那么这些新被创建的“进程”就是所谓的线程。
Notice:linux操作系统下没有线程概念,都是进程模拟出来的,一个线程内部包含多个线程。所谓的就是进程生成了多个(TCB)多个执行流这里叫作线程控制块,分别去完成不同的任务。
这里cpu虽然看到的还是task_struct.但是这些结构体都要比进程更轻量化了。
所以说线程是比进程更细的的执行流
2丶多线程优缺点
1丶优点
1.创建一个新线程的代价要比创建一个新进程小得多
在创建进程的时候,就需要创建相应的进程地址空间,页表,加载相应的代码和数据;而创建线程只要创建一个PCB,分配进程的资源即可;
2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程之间的切换,只需要切换线程的上下文,不需要更新页表,加载有效数据;
3.线程占用的资源要比进程少很多
线程本身就不是主要申请资源的角色,只是分担进程的资源;
4.能充分利用多处理器的可并行数量
5.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
计算密集型应用:比如加密、大数据运算等,主要使用的是CPU资源;贴合实际情况,比如我们经常用的好压,你解压一个文件,它就要涉及到大量的解压算法;
7.I/O密集型应用,为了提高性能,将I/O等待时间操作重叠。线程可以同时等待不同的I/O操作。
I/O密集型应用:比如网络下载、云盘、ssh、在线直播、看电影等,主要使用得到是内存和I/O资源;
2丶缺点
性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。比如:进程之间是相互独立的,我们打开各种软件,一个软件的崩溃并不会影响其他软件,变相的也就增加了进程的健壮性,而线程就不同了,因为大部分资源都是共享的,一个线程的崩溃就会导致其他所有线程崩溃,进而导致整个进程崩溃;
缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多
二丶线程的特性
进程是资源分配的基本单位;线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器(存储自己的上下文信息)
- 栈(每个线程都有临时数据,都需要压栈出栈,各自独立)
- errno
- 信号屏蔽字
- 调度优先级
共享同一地址空间,因此代码段(Text Segment)、数据段(Data Segment)都是共享的:
- 如果定义一个函数,在各线程中都可以调用;
- 如果定义一个全局变量,在各线程中都可以访问到;
- 除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
三丶线程简单控制
1.线程的生成
要生成线程需要包含线程库,pthread.h
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数解读:
- thread:获取创建成功的线程ID,该参数是一个输出型参数
- attr:设置线程的属性,attr为NULL表示使用默认属性
- start_routine:是个函数地址,线程启动后要执行的函数
- arg:传给线程启动函数的参数
返回值
- 成功返回0
- 失败返回错误码
#include<pthread.h> #include<stdio.h> #include<unistd.h> #include<stdlib.h> void* thread_run(void*arg) { while(1) { printf("thread id:%d\n",pthread_self()); sleep(2); } } int main() { pthread_t tid; pthread_create(&tid,NULL,thread_run,"new thread"); while(1) { printf("main thread id: %d\n",pthread_self()); sleep(1); } return 0; }
这里观察到主线程给PWD是个进程PID一样的这就得出以上的结论,进程包含多个线程
线程生成后状态:
和进程是一样的,线程也需要被关心是否正常退出,在执行过程中是否出现异常
线程ID获取:
- 创建线程时通过输出型参数获得。
- 通过调用pthread_self函数获得
我们可以一次生成多个线程
#include<stdio.h> #include<pthread.h> #include<unistd.h> void* thread_run(void* args) { while(1){ sleep(1); } } int main() { pthread_t tid[5]; int i; for(i = 0; i < 5; i++){ pthread_create(tid + i, NULL, thread_run, (void*)"new thread"); } while(1){ printf("I am main thread ID, %lu\n",pthread_self()); printf("################# begin ################\n"); int i; for(i = 0; i < 5; i++){ printf("I creat thread [%d] is: %lu\n", i, tid[i]); } printf("################# end #################\n"); sleep(1); } return 0; }
![]()
2.线程等待
和进程一样我们也需要线程的状态
参数解读:
- thread:被等待线程的ID
- retval:它是一个输出型参数,用来获取新线程退出的时候,函数的返回值;新线程函数的返回值是void*,所以要获取一级指针的值,就需要二级指针,也就是void**;
返回值:
- 成功返回0
- 失败返回错误码
如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
如果thread线程被别的线程调用pthread_ cancel异常终掉,retval所指向的单元里存放的是常数PTHREAD_ CANCELED。
如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
void* thread_run(void* args)
{
int num = *(int*)args;
while(1){
printf("I am new thread [%d], I creat thread ID: %lu\n", num, pthread_self());
sleep(3);
break;
}
return (void*)111;
}
int main()
{
pthread_t tid[1];
int i;
for(i = 0; i < 1; i++){
pthread_create(tid + i, NULL, thread_run, (void*)&i);
sleep(1);
}
void* status = NULL;
pthread_join(tid[0], &status);
printf("ret: %d\n", status);
return 0;
}
我们知道进程退出时有三种状态:
1.代码跑完,结果正确
2.代码跑完,结果错误
3.代码异常终止那么线性也是一样的,这里就存在一个问题,刚刚上面的代码,是获取线程的退出码的,那么代码异常终止,线程需要获取吗?
答案:不需要;pthread_join函数无法获取到线程异常退出时的信息。因为线程是进程内的一个执行流,如果进程中的某个线程崩溃了,那么整个进程也会因此而崩溃,此时我们根本没办法执行pthread_join函数,因为整个进程已经退出了;
我们设置一个经典的野指针问题👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇
👇
void*thread_run(void *args) { int num=*(int*)args; while(1) { printf("我是新线程[%d],我创建的线程ID是: %lu\n",num,pthread_self()); sleep(1); if(num==3) { printf("thread number : %d quit\n",num); int *p=NULL; *p=100; } } } int main() { pthread_t tid[5]; int i=0; for(i=0;i<5;i++) { pthread_create(tid+i,NULL,thread_run,(void*)&i); sleep(1); } while(1) { printf("i am main thread,我thread ID:%lu\n",pthread_self()); int i=0; printf("#######################begin################\n"); for(i=0;i<5;i++) { printf("我创建的线程[%d]是:%lu \n",i,tid[i]); } printf("#######################end################\n"); sleep(1); } return 0; }
在第三个线程创建后出现错误,我们观察是否能收到异常状态
3号线程错误直接崩溃,整个进程就直接退出了,后面的线程也就都执行不了了~所以这个join函数并收不带推出信息~
3.线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit函数终止自己。
- 一个线程可以调用pthread_ cancel函数终止同一进程中的另一个线程。
函数:
![]()
#define NUM 5 void* thread_run(void* args) { int num = *(int*)args; while(1){ printf("I am new thread [%d], I creat thread ID: %lu\n", num, pthread_self()); sleep(3); break; } //exit(111); //1 pthread_exit((void*)111); //2 } int main() { pthread_t tid[NUM]; int i; for( i = 0; i < NUM; i++){ pthread_create(tid + i, NULL, thread_run, (void*)&i); sleep(1); } void* status = NULL; for( i = 0; i < NUM; i++){ pthread_join(tid[i], &status); printf("I am thread[%d], I am code: %d\n",i ,status); } while(1){ printf("I am main thread\n"); sleep(1); } return 0; }
新线程全部终止自己
使用一个线程终止另一个线程
线程终止的函数:pthread_ cancel(让别的线程终止)
int pthread_cancel(pthread_t thread);
参数说明:thread:被取消线程的ID。
返回值说明:线程取消成功返回0,失败返回错误码。
线程是可以取消自己的,取消成功的线程的退出码一般是-1;void* thread_run(void* args) { while(1){ printf("I am new thread [%s], I creat thread ID: %lu\n", (const char*)args, pthread_self()); sleep(1); } } int main() { pthread_t tid; pthread_create(&tid, NULL, thread_run, (void*)"thread 1"); sleep(3); printf("wait new thread...\n");//主线程休眠3秒后,提示在等待线程 sleep(10); printf("cancel wait new thread...\n");//主线等待10秒之后,提示取消等待 pthread_cancel(tid); //调用函数,取消新线程 void* status = NULL; pthread_join(tid, &status); //和获取新线程退出时的退出码 printf("I am thread: %d, I am code: %d\n",tid ,(int)status); return 0; }
类似的使用新线程取消主线程,这时候主线程就会处于僵尸状态
4.线程分离
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
- 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
函数:int pthread_detach(pthread_t thread);
example:分离新线程
void* thread_run(void* args) { pthread_detach(pthread_self());//让新线程分离 while(1){ printf("I am new thread [%s], I creat thread ID: %lu\n", (const char*)args, pthread_self()); sleep(2); break; } return (void*)111; } int main() { pthread_t tid; int ret = 0; pthread_create(&tid, NULL, thread_run, (void*)"thread 1"); sleep(1); void* status = NULL; ret = pthread_join(tid, &status); printf("ret: %d, status: %d\n",ret ,status); sleep(3); return 0; }