●🧑个人主页:你帅你先说.
●📃欢迎点赞👍关注💡收藏💖
●📖既选择了远方,便只顾风雨兼程。
●🤟欢迎大家有问题随时私信我!
●🧐版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。
1. Linux线程概念
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程。
- 线程在进程内部运行,本质是在进程地址空间内运行。
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
⭐️线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。(加密,大数据运算等)
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。(网络下载、云盘、ssh、在线直播、看电影)
⭐️线程的缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。 - 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 - 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
值得注意的是Linux在设计线程时,并没有单独设计一个结构体为线程,而是用进程的pcb来模拟线程。这样的好处是不用维护复杂的进程和线程的关系,不用单独为线程设计任何算法,直接使用进程的一套相关的方法。OS只需要聚焦在线程间的资源分配上就可以了
。
进程是资源分配的基本单位。
线程是CPU调度的基本单位,承担进程资源的一部分的基本实体。
线程共享进程数据,但也拥有自己的一部分数据。(pcb,栈,上下文)
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
2.线程控制
2.1线程创建
功能:创建一个新的线程
原型: 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 <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* args)
{
while(1)
{
//输出新线程
printf("我是新线程[%s],我创建的线程ID是: %lu\n",(const char*)args,pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid;
//创建新线程
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
while(1)
{
printf("我是主线程,我创建的线程ID是: %lu\n",tid);
sleep(1);
}
}
注意,pthread_create
这个接口不是系统提供的,而是第三方库,所以我们还需要链接pthread
库
在makefile中应该这样
my_thread:mythread.c
gcc -o $@ $^ -lpthread
.PHONY:clean
clean:
rm -f mythread
我们发现主线程的PID和它的LWP(轻量级进程)是一样的,而新的线程PID和主线程一样,LWP不同。
若创建多个线程就可以执行多个任务,但创建多线程会导致健壮性下降,即其中一个线程出现问题,其它线程会受牵连。
不知道大家有没有发现,LWP好像和我们打印出的ID不一样。其实我们查看到的线程ID是pthread库的线程id,不是Linux内核中的LWP,pthread库的线程id是一个内存地址。
2.2线程等待
一般而言,线程也是需要等待的,如果不等待,可能会导致类似于"僵尸进程"的问题。
功能:等待线程结束
原型: int pthread_join(pthread_t thread, void **retval);
参数:
thread:线程ID
retval:输出型参数,它指向一个指针。
返回值:成功返回0;失败返回错误码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* args)
{
while(1)
{
//输出新线程ID
printf("我是新线程[%s],我创建的线程ID是: %lu\n",(const char*)args,pthread_self());
sleep(5);
break;
}
return (void*)666;//返回值可以是任何类型,但不能时临时变量。可以是int、对象的地址等
}
int main()
{
pthread_t tid;
//创建新线程
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
void* status = NULL;
//等待线程
pthread_join(tid,&status);
printf("ret: %d\n",(int)status);
}
有人可能会有疑问,线程异常的情况这个函数好像没有处理。实际上根本不需要,因为当这个线程崩掉了,其它线程也会跟着崩,这个时候轮不到线程来处理,而是进程来处理。
2.3线程终止
1.函数中return(main函数中退出return代表主线程和进程退出),其它线程函数return,只代表当前线程退出。
2.新线程通过pthread_exit终止自己。(不要用exit进行退出,exit的对象是进程,进程退出后所有线程也就退出了。)
3.通过pthread_cancel函数取消目标线程。
功能:线程终止
原型: void pthread_exit(void *value_ptr);
参数:
retval:输出型参数,它指向一个指针。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* args)
{
while(1)
{
//输出新线程ID
printf("我是新线程[%s],我创建的线程ID是: %lu\n",(const char*)args,pthread_self());
sleep(5);
break;
}
pthread_exit((void*)123);
}
int main()
{
pthread_t tid;
//创建新线程
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
void* status = NULL;
//等待线程
pthread_join(tid,&status);
printf("ret: %d\n",(int)status);
}
功能:取消一个执行中的线程
原型: int pthread_cancel(pthread_t thread); 参数:
thread:线程ID
返回值:成功返回0;失败返回错误码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* args)
{
while(1)
{
//输出新线程ID
printf("我是新线程[%s],我创建的线程ID是: %lu\n",(const char*)args,pthread_self());
sleep(2);
//注意这里必须是死循环,若是循环正常结束则可能导致线程不是被取消而是正常退出的。
}
}
int main()
{
pthread_t tid;
//创建新线程
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
printf("wait sub thread...\n");
sleep(5);
printf("cancel sub thread..\n");
//取消线程
pthread_cancel(tid);
void* status = NULL;
//等待线程
pthread_join(tid,&status);
printf("ret: %d\n",(int)status);
}
线程成功被取消,退出码为-1
。-1实际上在系统中是这样定义的。
#define PTHREAD_CANCELED (void*)-1
这个函数也可以用来取消主线程,但一般不会那样做,否则会出现类似于"僵尸进程"的情况。
2.4线程分离
分离之后的线程不需要被等待,运行完毕之后,会自动释放。
功能:分离一个执行中的线程
原型: int pthread_detach(pthread_t thread); 参数:
thread:线程ID
返回值:成功返回0;失败返回错误码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* thread_run(void* args)
{
pthread_detach(pthread_self());//线程分离
while(1)
{
//输出新线程ID
printf("我是新线程[%s],我创建的线程ID是: %lu\n",(const char*)args,pthread_self());
sleep(2);
break;
}
return (void*)666;
}
int main()
{
pthread_t tid;
//创建新线程
pthread_create(&tid,NULL,thread_run,(void*)"new thread");
printf("wait sub thread...\n");
sleep(1);
void* status = NULL;
//等待线程
ret = pthread_join(tid,&status);
printf("ret: %d\n",ret);
}
最终会发现ret是非0,这就说明线程不是正常退出的,所以被分离的线程是不能等待的。
因为多个线程是共享地址空间的,也就是很多资源都是共享的。优点是通信方便,缺点是缺乏访问控制。因为一个线程的操作问题,给其他线程造成了不可控,或者引起奔溃、异常,逻辑不正确等这种现象,这就导致了线程的安全问题。如果创建一个函数想要没有线程安全问题,就不要使用全局变量、STL、malloc、new等会在全局内有效的数据
。因为如果都是局部变量,线程有自己的独立栈结构。
一张图总结线程与其它内核结构的关系。
2.5线程互斥
- 临界资源:被线程共享访问的资源。
- 临界区:代码中访问临界资源的代码
- 互斥或同步是对临界区进行保护的功能。本质是对临界资源的保护
- 互斥:在任意时刻,只允许一个执行流访问某